<?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>Manticore Search 29.9.0: Chunked auto-embeddings and mmap columnar access</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:05:04 +0000</pubDate>
      <link>https://dev.to/sanikolaev/manticore-search-2990-chunked-auto-embeddings-and-mmap-columnar-access-28ic</link>
      <guid>https://dev.to/sanikolaev/manticore-search-2990-chunked-auto-embeddings-and-mmap-columnar-access-28ic</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://mnt.cr/go/c72RPs" rel="noopener noreferrer"&gt;https://mnt.cr/go/c72RPs&lt;/a&gt; on September 11, 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  Manticore Search 29.9.0: Chunked auto-embeddings and mmap columnar access
&lt;/h1&gt;

&lt;p&gt;Manticore Search 29.9.0 adds chunked and multi-vector auto-embeddings, embedding input limits, UTF-8 identifiers, mmap columnar access by default, and fixes for hybrid search, KNN, bulk ingestion, and grouping.&lt;/p&gt;

&lt;p&gt;[Manticore Search 29.9.0](&lt;a href="https://mnt.cr/go/LfXmU1" rel="noopener noreferrer"&gt;https://mnt.cr/go/LfXmU1&lt;/a&gt; has been released. The largest change is in auto-embeddings: long documents can now be split into searchable chunks, and a document can keep several vectors instead of compressing all of its content into one. This release also makes &lt;code&gt;mmap&lt;/code&gt; the default access mode for columnar attributes, adds safer controls for embedding workloads, UTF-8 identifiers, better backup support, and fixes across hybrid search, KNN, bulk ingestion, grouped search, and schema changes.&lt;/p&gt;

&lt;p&gt;This post covers everything shipped after 29.0.2, from &lt;strong&gt;29.0.3 through 29.9.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;❤️ We’d like to thank [@tudorvasinca](&lt;a href="https://mnt.cr/go/Vn5RXy" rel="noopener noreferrer"&gt;https://mnt.cr/go/Vn5RXy&lt;/a&gt; for their work on [PR #4857](&lt;a href="https://mnt.cr/go/DdBMb7" rel="noopener noreferrer"&gt;https://mnt.cr/go/DdBMb7&lt;/a&gt;, [PR #4859](&lt;a href="https://mnt.cr/go/jZePzd" rel="noopener noreferrer"&gt;https://mnt.cr/go/jZePzd&lt;/a&gt;, and [PR #4873](&lt;a href="https://mnt.cr/go/eQNGL6" rel="noopener noreferrer"&gt;https://mnt.cr/go/eQNGL6&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;There is no release-wide mandatory data migration. Existing tables and configuration can be upgraded normally, but a few fixes need follow-up if the earlier behavior already affected your data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;German sharp-s normalization is opt-in. Switching an existing table to &lt;code&gt;lemmatize_de_v2&lt;/code&gt; or &lt;code&gt;lemmatize_de_v2_all&lt;/code&gt; changes indexed terms, so rebuild a plain table or replay documents into a new RT table.&lt;/li&gt;
&lt;li&gt;Authenticated &lt;code&gt;BACKUP&lt;/code&gt; introduces the &lt;code&gt;backup&lt;/code&gt; authorization action. Before downgrading to 29.3.12 or earlier, remove any &lt;code&gt;backup&lt;/code&gt; grants.&lt;/li&gt;
&lt;li&gt;Columnar attributes now use &lt;code&gt;mmap&lt;/code&gt; by default instead of buffered &lt;code&gt;file&lt;/code&gt; reads. Set &lt;code&gt;access_columnar_attrs='file'&lt;/code&gt; explicitly if you need to retain the previous access mode.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Long documents can keep more than one embedding
&lt;/h2&gt;

&lt;p&gt;The old auto-embedding path produced one vector per document and truncated text that did not fit the model input window. That works for titles and short descriptions, but it means a relevant paragraph near the end of a long article may never reach the index.&lt;/p&gt;

&lt;p&gt;Manticore Search now supports five [chunking strategies](&lt;a href="https://mnt.cr/go/BXIJ8c" rel="noopener noreferrer"&gt;https://mnt.cr/go/BXIJ8c&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;truncate&lt;/code&gt; keeps the previous behavior and remains the default.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mean&lt;/code&gt; embeds every chunk and averages the results into one vector.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fixed&lt;/code&gt; splits text into fixed-size token windows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recursive&lt;/code&gt; prefers paragraph, line, sentence, and space boundaries in that order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sentence&lt;/code&gt; groups complete sentences up to the configured limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The multi-vector strategies use [&lt;code&gt;float_vector_array&lt;/code&gt;](&lt;a href="https://mnt.cr/go/Y31JKu" rel="noopener noreferrer"&gt;https://mnt.cr/go/Y31JKu&lt;/a&gt; Each chunk competes independently during KNN search, but Manticore returns the document once and uses its closest chunk for &lt;code&gt;knn_dist()&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;articles&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;content&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;chunks&lt;/span&gt; &lt;span class="n"&gt;float_vector_array&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;'cosine'&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;'Xenova/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,content'&lt;/span&gt;
    &lt;span class="n"&gt;chunk_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sentence'&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'256'&lt;/span&gt; &lt;span class="n"&gt;overlap_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'32'&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;articles&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;content&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;'Rotating certificates'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'A long guide with many sections ...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&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;knn_dist&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;articles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&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;'how do I rotate a certificate'&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;MAX_TOKENS&lt;/code&gt;, &lt;code&gt;OVERLAP_TOKENS&lt;/code&gt;, and &lt;code&gt;MAX_CHUNKS&lt;/code&gt; control chunk size, shared context at boundaries, and the maximum vector count. Declare a model-backed &lt;code&gt;float_vector_array&lt;/code&gt; when creating the table: adding one later with &lt;code&gt;ALTER TABLE ... ADD COLUMN&lt;/code&gt; and rebuilding its embeddings are not supported yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a ceiling on local embedding work
&lt;/h2&gt;

&lt;p&gt;Long-context models can make a single large input unexpectedly expensive, especially on CPU. The new [&lt;code&gt;MAX_INPUT_TOKENS&lt;/code&gt;](&lt;a href="https://mnt.cr/go/sX4dok" rel="noopener noreferrer"&gt;https://mnt.cr/go/sX4dok&lt;/a&gt; column option caps how much of each input is sent to a local embedding model:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="n"&gt;MAX_INPUT_TOKENS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'512'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change applies to embeddings generated afterward; existing vectors stay as they are. Set it during &lt;code&gt;CREATE TABLE&lt;/code&gt; or change it later without re-embedding the table. A value of &lt;code&gt;0&lt;/code&gt;, or leaving the option out, uses the model's own limit.&lt;/p&gt;

&lt;p&gt;This release also fixes two less visible problems around this path. Models no longer remain cached after an embedding column is modified, and configurations with different &lt;code&gt;API_TIMEOUT&lt;/code&gt; or &lt;code&gt;MAX_INPUT_TOKENS&lt;/code&gt; values no longer collide and reuse the wrong cached model.&lt;/p&gt;




&lt;h2&gt;
  
  
  UTF-8 identifiers and better German matching
&lt;/h2&gt;

&lt;p&gt;Table, field, and attribute names now follow one consistent [safe UTF-8 identifier syntax](&lt;a href="https://mnt.cr/go/mfKS2T" rel="noopener noreferrer"&gt;https://mnt.cr/go/mfKS2T&lt;/a&gt; RT, percolate, distributed, template, and plain tables can use localized names such as Chinese or Cyrillic identifiers across DDL, expressions, field selectors, and inferred source schemas.&lt;/p&gt;

&lt;p&gt;German AOT morphology also gains opt-in sharp-s normalization. With &lt;code&gt;charset_table=non_cont,german&lt;/code&gt; and &lt;code&gt;morphology=lemmatize_de_v2&lt;/code&gt; or &lt;code&gt;lemmatize_de_v2_all&lt;/code&gt;, forms such as &lt;code&gt;Straße&lt;/code&gt;, &lt;code&gt;Strasse&lt;/code&gt;, and &lt;code&gt;STRAẞE&lt;/code&gt; match in ordinary whole-word searches. If &lt;code&gt;index_exact_words=1&lt;/code&gt; is enabled, exact-word queries can still distinguish the &lt;code&gt;ß&lt;/code&gt; and &lt;code&gt;ss&lt;/code&gt; forms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Better load testing and backups
&lt;/h2&gt;

&lt;p&gt;[&lt;code&gt;manticore-load&lt;/code&gt;](&lt;a href="https://mnt.cr/go/cnoLS6" rel="noopener noreferrer"&gt;https://mnt.cr/go/cnoLS6&lt;/a&gt; can now benchmark through the HTTP JSON API with &lt;code&gt;--http&lt;/code&gt;. Writes use &lt;code&gt;/bulk&lt;/code&gt;, searches use &lt;code&gt;/search&lt;/code&gt;, and &lt;code&gt;--table&lt;/code&gt; selects the target table. Its reports now include local &lt;code&gt;searchd&lt;/code&gt; RSS during the run plus peak RSS, disk, and CPU statistics at the end, including aggregate monitoring for multi-command workloads.&lt;/p&gt;

&lt;p&gt;[Manticore Backup](&lt;a href="https://mnt.cr/go/t4acW4" rel="noopener noreferrer"&gt;https://mnt.cr/go/t4acW4&lt;/a&gt; now works with authenticated Manticore Search installations through username/password or bearer-token credentials. SQL [&lt;code&gt;BACKUP&lt;/code&gt;](&lt;a href="https://mnt.cr/go/6dxdrT" rel="noopener noreferrer"&gt;https://mnt.cr/go/6dxdrT&lt;/a&gt; has a dedicated authorization action and checks read access to the selected tables.&lt;/p&gt;

&lt;p&gt;S3 backups and restores can also use the AWS SDK credential provider chain when static keys are not set. That includes IRSA, shared credentials, ECS task roles, and EC2 instance profiles. Temporary credentials can supply &lt;code&gt;AWS_SESSION_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Columnar attributes use mmap by default
&lt;/h2&gt;

&lt;p&gt;Manticore Search now defaults [&lt;code&gt;access_columnar_attrs&lt;/code&gt;](&lt;a href="https://mnt.cr/go/sdnJ5l" rel="noopener noreferrer"&gt;https://mnt.cr/go/sdnJ5l&lt;/a&gt; to &lt;code&gt;mmap&lt;/code&gt;. The operating system maps and caches &lt;code&gt;*.spc&lt;/code&gt; columnar-attribute files on demand, without prereading the whole file at startup. The previous buffered path remains available with &lt;code&gt;access_columnar_attrs='file'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt; also reopens replaced columnar storage with the configured access mode, so an altered table no longer falls back to a different reader than the one requested.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vector and grouped-search fixes
&lt;/h2&gt;

&lt;p&gt;Several fixes target queries that were valid but could return incomplete results or fail under a particular table layout:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distributed and sharded KNN queries with a local shard no longer rescore merged 1-bit-quantized results twice, which could crash the coordinator or return the wrong nearest neighbor. ([Issue #4791](&lt;a href="https://mnt.cr/go/LfXW1t" rel="noopener noreferrer"&gt;https://mnt.cr/go/LfXW1t&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;KNN queries with additional filters avoid a redundant &lt;code&gt;knn_dist&lt;/code&gt; prefilter when HNSW already excludes documents without vectors. ([PR #4861](&lt;a href="https://mnt.cr/go/bZtpIJ" rel="noopener noreferrer"&gt;https://mnt.cr/go/bZtpIJ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LENGTH()&lt;/code&gt; on a &lt;code&gt;float_vector_array&lt;/code&gt; now reports the number of vectors rather than its internal storage-word count. ([PR #4879](&lt;a href="https://mnt.cr/go/8be2oV" rel="noopener noreferrer"&gt;https://mnt.cr/go/8be2oV&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hybrid search with &lt;code&gt;GROUP BY&lt;/code&gt; retains all buckets, including MVA groups, and follows both final and within-group ordering. ([Issue #4639](&lt;a href="https://mnt.cr/go/b5Hmg8" rel="noopener noreferrer"&gt;https://mnt.cr/go/b5Hmg8&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hybrid filters on &lt;code&gt;weight()&lt;/code&gt; and expressions or aliases derived from it now run after fusion against the final text weight instead of being ignored. Weight-dependent filters inside &lt;code&gt;OR&lt;/code&gt; trees remain unsupported and return an explicit error. ([Issue #4889](&lt;a href="https://mnt.cr/go/OMu9qt" rel="noopener noreferrer"&gt;https://mnt.cr/go/OMu9qt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Multi-chunk RT grouping no longer risks duplicate groups, incorrect split counts, or a hang while ordering &lt;code&gt;COUNT(DISTINCT ...)&lt;/code&gt; results. ([Issue #4856](&lt;a href="https://mnt.cr/go/KtrFXK" rel="noopener noreferrer"&gt;https://mnt.cr/go/KtrFXK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Document-ID filters whose signed representation is negative now follow the lookup index's unsigned ordering. ([Issue #4774](&lt;a href="https://mnt.cr/go/nEO1vc" rel="noopener noreferrer"&gt;https://mnt.cr/go/nEO1vc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are crash fixes here too: a second hybrid-search statement in a multi-statement request ([PR #4864](&lt;a href="https://mnt.cr/go/RJ5hGi" rel="noopener noreferrer"&gt;https://mnt.cr/go/RJ5hGi&lt;/a&gt;, distributed JSON aggregation sorted by a string attribute ([Issue #4822](&lt;a href="https://mnt.cr/go/Jdpl0M" rel="noopener noreferrer"&gt;https://mnt.cr/go/Jdpl0M&lt;/a&gt;, and dropping a table during auto-embedding precommit ([Issue #4860](&lt;a href="https://mnt.cr/go/Edp7gc" rel="noopener noreferrer"&gt;https://mnt.cr/go/Edp7gc&lt;/a&gt; are all handled safely now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulk ingestion behaves predictably
&lt;/h2&gt;

&lt;p&gt;Elasticsearch-compatible &lt;code&gt;/_bulk&lt;/code&gt; requests now return HTTP &lt;code&gt;200&lt;/code&gt; once a batch has been processed, while individual failures remain visible through &lt;code&gt;errors: true&lt;/code&gt; and per-item statuses. Duplicate &lt;code&gt;create&lt;/code&gt; actions return per-item &lt;code&gt;409&lt;/code&gt; &lt;code&gt;version_conflict_engine_exception&lt;/code&gt; errors, including duplicates within the same batch. This prevents clients such as Fluent Bit from retrying writes that already succeeded.&lt;/p&gt;

&lt;p&gt;Fixed-length gzip-compressed &lt;code&gt;/bulk&lt;/code&gt; bodies are also decoded correctly when they arrive across multiple socket reads. And if native bulk processing fails because the target table does not exist, the request can again reach Manticore's auto-schema fallback with valid NDJSON while preserving the expected bulk response envelope.&lt;/p&gt;




&lt;h2&gt;
  
  
  More reliability fixes
&lt;/h2&gt;

&lt;p&gt;The rest of the release closes a broad set of operational and compatibility problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;searchd --stopwait&lt;/code&gt; no longer hangs while a sharded table is being rebalanced after a node rejoins. ([Issue #3905](&lt;a href="https://mnt.cr/go/ZVFkNe" rel="noopener noreferrer"&gt;https://mnt.cr/go/ZVFkNe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Compatible older binlogs replay safely during upgrade and completed RT chunks are published before clean shutdown. ([Issue #4808](&lt;a href="https://mnt.cr/go/O0ZRHJ" rel="noopener noreferrer"&gt;https://mnt.cr/go/O0ZRHJ&lt;/a&gt; Fatal replay diagnostics also name the relevant recovery flag. ([Issue #4811](&lt;a href="https://mnt.cr/go/pzGwEm" rel="noopener noreferrer"&gt;https://mnt.cr/go/pzGwEm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;indexer&lt;/code&gt; creates missing parent directories for plain-table paths when the nearest existing parent is writable. ([Issue #4793](&lt;a href="https://mnt.cr/go/eIFSQf" rel="noopener noreferrer"&gt;https://mnt.cr/go/eIFSQf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;UUID document IDs no longer cause stored text fields to come back empty. ([Issue #4833](&lt;a href="https://mnt.cr/go/otelYC" rel="noopener noreferrer"&gt;https://mnt.cr/go/otelYC&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE ... RENAME&lt;/code&gt; preserves hidden remote embedding API keys without exposing them in &lt;code&gt;SHOW CREATE TABLE&lt;/code&gt;. ([Issue #4842](&lt;a href="https://mnt.cr/go/XkT4iw" rel="noopener noreferrer"&gt;https://mnt.cr/go/XkT4iw&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sequel Ace 5.3.1+ compatibility probes work again. ([Issue #4828](&lt;a href="https://mnt.cr/go/zjvXbT" rel="noopener noreferrer"&gt;https://mnt.cr/go/zjvXbT&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;JSON &lt;code&gt;/search&lt;/code&gt; keeps distances for negated &lt;code&gt;NEAR&lt;/code&gt; and proximity operators. ([Issue #4784](&lt;a href="https://mnt.cr/go/NgB0Q7" rel="noopener noreferrer"&gt;https://mnt.cr/go/NgB0Q7&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Internal string-sort helper columns no longer leak from &lt;code&gt;LEFT JOIN&lt;/code&gt; results. ([Issue #4788](&lt;a href="https://mnt.cr/go/rxNeFg" rel="noopener noreferrer"&gt;https://mnt.cr/go/rxNeFg&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Malformed binary API &lt;code&gt;SEARCH&lt;/code&gt; element counts are rejected instead of terminating &lt;code&gt;searchd&lt;/code&gt;. ([PR #4790](&lt;a href="https://mnt.cr/go/ufPVLK" rel="noopener noreferrer"&gt;https://mnt.cr/go/ufPVLK&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the complete list, see the [Version 29.9.0 changelog](&lt;a href="https://mnt.cr/go/gl5v6N" rel="noopener noreferrer"&gt;https://mnt.cr/go/gl5v6N&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;Install or upgrade Manticore Search with the [installation guide](&lt;a href="https://mnt.cr/go/XPyNrv" rel="noopener noreferrer"&gt;https://mnt.cr/go/XPyNrv&lt;/a&gt; Review the upgrade notes above if you use columnar attributes, German AOT morphology, or authenticated backups.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Join our [Slack](&lt;a href="https://mnt.cr/go/v4HPeK" rel="noopener noreferrer"&gt;https://mnt.cr/go/v4HPeK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit the [Forum](&lt;a href="https://mnt.cr/go/R7u2vk" rel="noopener noreferrer"&gt;https://mnt.cr/go/R7u2vk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Report issues or suggest features on [GitHub](&lt;a href="https://mnt.cr/go/CURvTu" rel="noopener noreferrer"&gt;https://mnt.cr/go/CURvTu&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>search</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Explore Manticore Data with OpenSearch Dashboards</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Tue, 08 Sep 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/sanikolaev/explore-manticore-data-with-opensearch-dashboards-59db</link>
      <guid>https://dev.to/sanikolaev/explore-manticore-data-with-opensearch-dashboards-59db</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://manticoresearch.com/blog/manticore-opensearch-dashboards-integration/" rel="noopener noreferrer"&gt;https://manticoresearch.com/blog/manticore-opensearch-dashboards-integration/&lt;/a&gt; on September 8, 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  Explore Manticore Data with OpenSearch Dashboards
&lt;/h1&gt;

&lt;p&gt;Connect OpenSearch Dashboards to Manticore Search, explore data in Discover, and build visualizations and dashboards using a familiar interface.&lt;/p&gt;

&lt;p&gt;OpenSearch Dashboards provides a familiar visual interface for exploring data, building charts, and assembling interactive dashboards. Although it is normally paired with OpenSearch, you can also connect it directly to Manticore Search.&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%2F7wd6g799m5lqto6r8kv1.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%2F7wd6g799m5lqto6r8kv1.png" alt="Main view" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The integration lets you keep Manticore as the search and analytics backend while using &lt;strong&gt;Discover&lt;/strong&gt;, &lt;strong&gt;Visualize&lt;/strong&gt;, and &lt;strong&gt;Dashboards&lt;/strong&gt; as the user interface. This is especially useful for log and event data: tools such as Logstash, Filebeat, Fluent Bit, and Vector can send data to Manticore, while OpenSearch Dashboards gives your team a convenient way to inspect it.&lt;/p&gt;

&lt;p&gt;In this tutorial, we will configure the connection, add a small sample dataset, explore it in Discover, and create a visualization.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the integration works
&lt;/h2&gt;

&lt;p&gt;OpenSearch Dashboards communicates with its backend through an HTTP API. Manticore exposes a compatible API on its HTTP listener, which uses port &lt;code&gt;9308&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;The compatibility layer is provided by the &lt;strong&gt;EmulateElastic&lt;/strong&gt; plugin in &lt;a href="https://manual.manticoresearch.com/Installation/Manticore_Buddy" rel="noopener noreferrer"&gt;Manticore Buddy&lt;/a&gt;. It handles the requests that OpenSearch Dashboards needs for startup, index discovery, searches, filters, and supported aggregations. Buddy normally starts automatically with &lt;code&gt;searchd&lt;/code&gt;, so a standard Manticore installation already has the required component.&lt;/p&gt;

&lt;p&gt;This is an API compatibility integration, not an embedded OpenSearch cluster. Manticore stores and queries the data, and OpenSearch Dashboards provides the visual interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;For this walkthrough, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A running Manticore Search instance with its HTTP endpoint available at &lt;code&gt;http://localhost:9308&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Manticore Buddy installed and running&lt;/li&gt;
&lt;li&gt;OpenSearch Dashboards &lt;strong&gt;3.4.0&lt;/strong&gt;, the currently tested and recommended version&lt;/li&gt;
&lt;li&gt;Manticore running in real-time mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other OpenSearch Dashboards versions may work, but they have not been tested to the same extent. The version reported by Manticore must also match the version of OpenSearch Dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Configure Manticore
&lt;/h2&gt;

&lt;p&gt;Open your Manticore configuration file and make sure that the HTTP listener is enabled. Set &lt;code&gt;kibana_version_string&lt;/code&gt; to the version of OpenSearch Dashboards that you plan to run:&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;listen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1:9308:http&lt;/span&gt;
    &lt;span class="py"&gt;pid_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/run/manticore/searchd.pid&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;kibana_version_string&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3.4.0&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name &lt;code&gt;kibana_version_string&lt;/code&gt; is retained for compatibility with the existing Elasticsearch-style API. OpenSearch Dashboards checks the backend version during startup, so a mismatch can produce warnings or prevent the application from starting.&lt;/p&gt;

&lt;p&gt;Restart Manticore after changing the configuration.&lt;/p&gt;

&lt;p&gt;If Manticore and OpenSearch Dashboards run on different hosts or in different containers, do not bind the HTTP listener only to &lt;code&gt;127.0.0.1&lt;/code&gt;. Bind it to an address that OpenSearch Dashboards can reach, and restrict access with your network or firewall configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configure OpenSearch Dashboards
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;opensearch_dashboards.yml&lt;/code&gt;. In a &lt;a href="https://docs.opensearch.org/latest/install-and-configure/install-opensearch/tar/" rel="noopener noreferrer"&gt;tarball installation&lt;/a&gt;, it is usually located at &lt;code&gt;config/opensearch_dashboards.yml&lt;/code&gt;; packages may place it at &lt;code&gt;/etc/opensearch-dashboards/opensearch_dashboards.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Point &lt;code&gt;opensearch.hosts&lt;/code&gt; to Manticore's HTTP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;opensearch.hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9308"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manticore does not provide the OpenSearch Security plugin. The corresponding Dashboards plugin must therefore be disabled.&lt;/p&gt;

&lt;p&gt;For a tarball installation, stop OpenSearch Dashboards and remove the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/opensearch-dashboards-plugin remove securityDashboards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run OpenSearch Dashboards in Docker, provide both settings through environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;OPENSEARCH_HOSTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;["http://manticore:9308"]'&lt;/span&gt;
  &lt;span class="na"&gt;DISABLE_SECURITY_DASHBOARDS_PLUGIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;manticore&lt;/code&gt; is the hostname or container name reachable from the OpenSearch Dashboards container. Using &lt;code&gt;localhost&lt;/code&gt; inside that container would refer to the container itself, not to Manticore.&lt;/p&gt;

&lt;p&gt;Start OpenSearch Dashboards and open &lt;a href="http://localhost:5601" rel="noopener noreferrer"&gt;http://localhost:5601&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add sample data to Manticore
&lt;/h2&gt;

&lt;p&gt;If you already have a real-time table, you can skip this step. Otherwise, connect to Manticore through its MySQL-compatible port:&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;-h127&lt;/span&gt;.0.0.1 &lt;span class="nt"&gt;-P9306&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a table for a small set of application events:&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;app_events&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;service&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;status_code&lt;/span&gt; &lt;span class="n"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;response_time&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;event_time&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a few documents:&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;app_events&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_time&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;'Request completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'catalog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1786924800&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Request completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checkout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1786928400&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Upstream timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checkout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&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="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1786932000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Product not found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'catalog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1786935600&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Request completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'catalog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1786939200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Payment rejected'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'payments'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1786942800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema gives us text to search, dimensions to group by, metrics to aggregate, and a timestamp for time-based charts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create an index pattern
&lt;/h2&gt;

&lt;p&gt;OpenSearch Dashboards uses an &lt;strong&gt;index pattern&lt;/strong&gt; to select the data source shown in Discover and visualizations. In this integration, the pattern matches a Manticore table name.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Management &amp;gt; Dashboards Management&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Index Patterns&lt;/strong&gt; and choose &lt;strong&gt;Create index pattern&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;app_events&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;event_time&lt;/code&gt; as the time field.&lt;/li&gt;
&lt;li&gt;Save the index pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;app_events&lt;/code&gt; fields should now be available in OpenSearch Dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Explore the data in Discover
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Discover&lt;/strong&gt; and select the &lt;code&gt;app_events&lt;/code&gt; index pattern. You can now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inspect individual documents&lt;/li&gt;
&lt;li&gt;Search the &lt;code&gt;message&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;Filter by fields such as &lt;code&gt;service&lt;/code&gt; or &lt;code&gt;status_code&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Change the time range&lt;/li&gt;
&lt;li&gt;Add and remove columns from the results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, add a filter where &lt;code&gt;status_code&lt;/code&gt; is greater than or equal to &lt;code&gt;400&lt;/code&gt; to focus on failed requests. You can then add &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;status_code&lt;/code&gt;, and &lt;code&gt;response_time&lt;/code&gt; as columns to get a compact error view.&lt;/p&gt;

&lt;p&gt;Simple Dashboard Query Language searches work with the integration. Advanced DQL features—including nested field searches, regular expressions, fuzzy searches, proximity searches, and term boosting—may not be compatible with Manticore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Build a visualization
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Visualize&lt;/strong&gt;, create a new visualization, and choose the &lt;code&gt;app_events&lt;/code&gt; index pattern. OpenSearch Dashboards can use the following bucket aggregations with Manticore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;terms&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;histogram&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;date_histogram&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;date_range&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supported metric aggregations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sum&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;avg&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a first chart, create a bar chart of events by service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;Count&lt;/code&gt; as the metric.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;Terms&lt;/code&gt; bucket using the &lt;code&gt;service&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;Apply the changes.&lt;/li&gt;
&lt;li&gt;Save the visualization as &lt;strong&gt;Events by service&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also create a time-series chart with a &lt;code&gt;Date Histogram&lt;/code&gt; on &lt;code&gt;event_time&lt;/code&gt;, or chart average latency by using the &lt;code&gt;avg&lt;/code&gt; metric on &lt;code&gt;response_time&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After saving your visualizations, open &lt;strong&gt;Dashboards&lt;/strong&gt;, create a dashboard, and add them. Filters applied at dashboard level let you focus the whole view on one service, status code, or time range.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is supported
&lt;/h2&gt;

&lt;p&gt;The integration covers the main data exploration workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Searching and filtering documents in Discover&lt;/li&gt;
&lt;li&gt;Creating index patterns for Manticore tables&lt;/li&gt;
&lt;li&gt;Building visualizations with supported bucket and metric aggregations&lt;/li&gt;
&lt;li&gt;Saving visualizations and combining them into dashboards&lt;/li&gt;
&lt;li&gt;Managing index patterns and saved objects in Dashboards Management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manticore also emulates the stack-level requests needed during OpenSearch Dashboards startup, including node version information, cluster settings, configuration objects, and index listings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;p&gt;OpenSearch Dashboards includes many features that depend on OpenSearch-specific APIs or field types. Those features are outside the scope of this integration.&lt;/p&gt;

&lt;p&gt;Unsupported field types include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Geographic and Cartesian fields such as &lt;code&gt;geo_point&lt;/code&gt;, &lt;code&gt;geo_shape&lt;/code&gt;, &lt;code&gt;xy_point&lt;/code&gt;, and &lt;code&gt;xy_shape&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Range types such as &lt;code&gt;integer_range&lt;/code&gt;, &lt;code&gt;ip_range&lt;/code&gt;, and &lt;code&gt;date_range&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Specialized search types such as &lt;code&gt;semantic&lt;/code&gt;, &lt;code&gt;rank_feature&lt;/code&gt;, and &lt;code&gt;percolator&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OpenSearch vector types such as &lt;code&gt;knn_vector&lt;/code&gt; and &lt;code&gt;sparse_vector&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Relational types such as &lt;code&gt;nested&lt;/code&gt; and &lt;code&gt;join&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Advanced string types such as &lt;code&gt;completion&lt;/code&gt; and &lt;code&gt;search_as_you_type&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plain &lt;code&gt;text&lt;/code&gt; and &lt;code&gt;keyword&lt;/code&gt; fields are supported. Manticore's own vector search remains available through its SQL and JSON APIs, but OpenSearch Dashboards cannot represent it through OpenSearch's vector field type.&lt;/p&gt;

&lt;p&gt;Nested aggregations—an &lt;code&gt;aggs&lt;/code&gt; block inside another &lt;code&gt;aggs&lt;/code&gt; block—are not supported. Metric functions are limited to those implemented by Manticore.&lt;/p&gt;

&lt;p&gt;OpenSearch-specific applications and administration tools are also not available, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Geospatial visualizations&lt;/li&gt;
&lt;li&gt;Observability and trace analytics&lt;/li&gt;
&lt;li&gt;Alerting and Anomaly Detection&lt;/li&gt;
&lt;li&gt;Security Analytics&lt;/li&gt;
&lt;li&gt;Index State Management and Index Management&lt;/li&gt;
&lt;li&gt;Performance Analyzer&lt;/li&gt;
&lt;li&gt;OpenSearch Security plugin workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These limitations do not affect the core Discover, visualization, and dashboard workflow described above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing in real data
&lt;/h2&gt;

&lt;p&gt;Once the connection works, you can replace the sample table with your own data pipeline. Manticore integrates with &lt;a href="https://manual.manticoresearch.com/Integration/Logstash" rel="noopener noreferrer"&gt;Logstash&lt;/a&gt;, &lt;a href="https://manual.manticoresearch.com/Integration/Filebeat" rel="noopener noreferrer"&gt;Filebeat&lt;/a&gt;, &lt;a href="https://manual.manticoresearch.com/Integration/Fluent_Bit" rel="noopener noreferrer"&gt;Fluent Bit&lt;/a&gt;, and &lt;a href="https://manual.manticoresearch.com/Integration/Vector" rel="noopener noreferrer"&gt;Vector&lt;/a&gt;. These tools can collect and transform logs or events before sending them to Manticore's Elasticsearch-compatible HTTP endpoint.&lt;/p&gt;

&lt;p&gt;The resulting workflow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An agent or pipeline collects your data.&lt;/li&gt;
&lt;li&gt;Manticore indexes it in real time.&lt;/li&gt;
&lt;li&gt;OpenSearch Dashboards queries Manticore over HTTP.&lt;/li&gt;
&lt;li&gt;Users explore the data and build dashboards in a familiar interface.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The OpenSearch Dashboards integration gives Manticore users a practical visual layer for log analysis and data exploration. With a small amount of configuration, you can search documents in Discover, build charts from supported aggregations, and combine them into dashboards while Manticore handles storage and query execution.&lt;/p&gt;

&lt;p&gt;For the latest compatibility details and configuration notes, see the &lt;a href="https://manual.manticoresearch.com/dev/Integration/Opensearch_Dashboards" rel="noopener noreferrer"&gt;OpenSearch Dashboards integration documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>search</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to return top-N from each group with GROUP_CONCAT()</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 10:00:05 +0000</pubDate>
      <link>https://dev.to/sanikolaev/how-to-return-top-n-from-each-group-with-groupconcat-1i3o</link>
      <guid>https://dev.to/sanikolaev/how-to-return-top-n-from-each-group-with-groupconcat-1i3o</guid>
      <description>&lt;p&gt;Originally posted by Stanislav Klinov on &lt;a href="https://manticoresearch.com/blog/group-concat-top-n-per-group/" rel="noopener noreferrer"&gt;https://manticoresearch.com/blog/group-concat-top-n-per-group/&lt;/a&gt; on September 4, 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  How to return top-N from each group with GROUP_CONCAT()
&lt;/h1&gt;

&lt;p&gt;A practical example of how to select several most recent items from each group in Manticore Search and combine them into a single string.&lt;/p&gt;

&lt;p&gt;Imagine a customer support screen. An agent searches for refund-related events and, instead of a long log, wants a short summary: one row per user, the total number of matches, and the &lt;strong&gt;five&lt;/strong&gt; most recent events. If more details are needed, the application can load them by ID.&lt;/p&gt;

&lt;p&gt;Two obvious approaches don't quite give us what we need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;regular &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; collects all values in the group together,&lt;/li&gt;
&lt;li&gt;while &lt;code&gt;GROUP N BY&lt;/code&gt; returns several rows per user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting with &lt;a href="https://manticoresearch.com/blog/manticore-search-28-6-6/" rel="noopener noreferrer"&gt;Manticore Search 28.6.6&lt;/a&gt;, values inside &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; can be sorted and limited to the number you need:&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;GROUP_CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;event_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's see how it works. We'll use SphinxQL and an explicit &lt;code&gt;GROUP BY&lt;/code&gt; - the new form of &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; doesn't work without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Events we'll work with
&lt;/h2&gt;

&lt;p&gt;Let's create an &lt;code&gt;activity&lt;/code&gt; table where each document is a single event. The event text is stored in &lt;code&gt;body&lt;/code&gt;, the user in &lt;code&gt;user_id&lt;/code&gt;, and the timestamp in &lt;code&gt;event_ts&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;activity&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;user_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;event_ts&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;event_type&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;activity&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&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;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund requested for order 501'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'requested'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user logged in'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000020&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund approved for order 501'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000030&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'approved'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1004&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund email sent for order 501'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000040&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund status checked for order 501'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000050&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'checked'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1006&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund payout queued for order 501'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000060&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1007&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund webhook retried for order 501'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000060&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'retried'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund requested for order 601'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'requested'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund approved for order 601'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'approved'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'shipping address changed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000035&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'shipping'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2004&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund payout queued for order 601'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000045&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund completed for order 601'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund requested for order 701'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000012&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'requested'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'invoice downloaded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000022&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'refund rejected for order 701'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1770000032&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A search for &lt;code&gt;refund&lt;/code&gt; will find six events for user 101, four for user 202, and two for user 303. We deliberately gave events 1006 and 1007 the same timestamp: later you will see why sorting also needs &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's wrong with the old approaches
&lt;/h2&gt;

&lt;p&gt;Let's start with regular &lt;code&gt;GROUP_CONCAT()&lt;/code&gt;. The response format is what we need - one row per user:&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;user_id&lt;/span&gt;&lt;span class="p"&gt;,&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;AS&lt;/span&gt; &lt;span class="n"&gt;matched_events&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;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;all_event_ids&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;activity&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;'refund'&lt;/span&gt;&lt;span class="p"&gt;)&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;user_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;matched_events&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_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;But for user 101, the string will contain all six IDs, for example &lt;code&gt;1001,1003,1004,1005,1006,1007&lt;/code&gt;, while we only need the five most recent ones. Also, without internal sorting, the order of the values is not guaranteed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------+----------------+-------------------------------+
| user_id | matched_events | all_event_ids                 |
+---------+----------------+-------------------------------+
|     101 |              6 | 1001,1003,1004,1005,1006,1007 |
|     202 |              4 | 2001,2002,2004,2005           |
|     303 |              2 | 3001,3003                     |
+---------+----------------+-------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another option is to ask &lt;code&gt;GROUP N BY&lt;/code&gt; to select the five most recent documents from each group:&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_ts&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;activity&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;'refund'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&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;event_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&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;user_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;The oldest event for user 101 disappears, but each remaining document is returned as a separate row. So instead of three rows, we get eleven: 5 + 4 + 2. This is convenient when the client needs the documents themselves, but it doesn't work for our compact summary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------+---------+------------+
| id   | user_id | event_ts   |
+------+---------+------------+
| 1007 |     101 | 1770000060 |
| 1006 |     101 | 1770000060 |
| 1005 |     101 | 1770000050 |
| 1004 |     101 | 1770000040 |
| 1003 |     101 | 1770000030 |
| 2005 |     202 | 1770000055 |
| 2004 |     202 | 1770000045 |
| 2002 |     202 | 1770000025 |
| 2001 |     202 | 1770000015 |
| 3003 |     303 | 1770000032 |
| 3001 |     303 | 1770000012 |
+------+---------+------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Combining only the five most recent IDs
&lt;/h2&gt;

&lt;p&gt;Now let's combine both steps: sort the documents directly inside &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; and limit the list to five values there as well.&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;user_id&lt;/span&gt;&lt;span class="p"&gt;,&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;AS&lt;/span&gt; &lt;span class="n"&gt;matched_events&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;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;event_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
        &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;recent_event_ids&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;activity&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;'refund'&lt;/span&gt;&lt;span class="p"&gt;)&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;user_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;matched_events&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------+----------------+--------------------------+
| user_id | matched_events | recent_event_ids         |
+---------+----------------+--------------------------+
|     101 |              6 | 1007,1006,1005,1004,1003 |
|     202 |              4 | 2005,2004,2002,2001      |
|     303 |              2 | 3003,3001                |
+---------+----------------+--------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, &lt;code&gt;MATCH('refund')&lt;/code&gt; selects refund events, then &lt;code&gt;GROUP BY user_id&lt;/code&gt; groups them by user. &lt;code&gt;COUNT(*)&lt;/code&gt; counts all matching events, while &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; takes only the first five from each group after sorting.&lt;/p&gt;

&lt;p&gt;The second sort key - &lt;code&gt;id DESC&lt;/code&gt; - is especially important here. Events 1006 and 1007 have the same &lt;code&gt;event_ts&lt;/code&gt;, so without it their relative order would be undefined. Sorting by ID ensures that event 1007 always comes first.&lt;/p&gt;

&lt;p&gt;Notice that the query has two &lt;code&gt;ORDER BY&lt;/code&gt; clauses. The one inside &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; determines the order of IDs in the string. The final &lt;code&gt;ORDER BY&lt;/code&gt; sorts the completed rows: first by the number of matches, then by &lt;code&gt;user_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The inner &lt;code&gt;LIMIT&lt;/code&gt; does not change &lt;code&gt;COUNT(*)&lt;/code&gt; or affect pagination of the overall result. So user 101 still has six matches even though only five IDs are shown next to it. That's exactly what we need for this summary.&lt;/p&gt;

&lt;p&gt;One more detail: &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; always returns a string, even when it contains numeric IDs. If the API needs to return an array of numbers or objects with several fields, the string has to be parsed on the client side, or a different response format should be used.&lt;/p&gt;

&lt;p&gt;The query works the same way with distributed tables. Manticore collects candidates from all local and remote tables, then selects the overall top-N for each group.&lt;/p&gt;

&lt;h2&gt;
  
  
  If a comma doesn't work
&lt;/h2&gt;

&lt;p&gt;By default, values are separated by commas. Sometimes a more readable string is useful - for example, showing the event type next to its ID. You can set a custom separator with &lt;code&gt;SEPARATOR&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;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&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;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_STRING&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="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&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;AS&lt;/span&gt; &lt;span class="n"&gt;recent_events&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;activity&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;'refund'&lt;/span&gt;&lt;span class="p"&gt;)&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;user_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;user_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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------+----------------------------------------------+
| user_id | recent_events                                |
+---------+----------------------------------------------+
|     101 | retried:1007 / queued:1006 / checked:1005    |
|     202 | completed:2005 / queued:2004 / approved:2002 |
|     303 | rejected:3003 / requested:3001               |
+---------+----------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this syntax, &lt;code&gt;SEPARATOR&lt;/code&gt; comes before &lt;code&gt;LIMIT&lt;/code&gt;. Manticore doesn't escape anything or add quotes: the result is a regular string, not a JSON array.&lt;/p&gt;

&lt;p&gt;This works well for &lt;code&gt;event_type&lt;/code&gt; because those values are controlled by the application. Be more careful with arbitrary text: if the separator appears in the data itself, the result can no longer be parsed reliably. In that case, it's better to return separate rows or use a structured format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the new approach doesn't work
&lt;/h2&gt;

&lt;p&gt;This form of &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; has several limitations. It works only in SQL queries with an explicit &lt;code&gt;GROUP BY&lt;/code&gt; and does not support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DISTINCT&lt;/code&gt;, &lt;code&gt;OFFSET&lt;/code&gt;, or combining multiple expressions at once;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JOIN&lt;/code&gt;, &lt;code&gt;FACET&lt;/code&gt;, outer &lt;code&gt;SELECT&lt;/code&gt; queries, or table functions;&lt;/li&gt;
&lt;li&gt;KNN and hybrid queries, or scroll;&lt;/li&gt;
&lt;li&gt;implicit grouping or equivalent aggregation syntax in the JSON API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its alias cannot be used in &lt;code&gt;HAVING&lt;/code&gt; or the final &lt;code&gt;ORDER BY&lt;/code&gt;. The groups themselves can still be sorted by the grouping key and regular aggregates - for example, by &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;COUNT(*)&lt;/code&gt;, as in the query above.&lt;/p&gt;

&lt;p&gt;Memory usage is another consideration. For each such expression, Manticore stores a separate top-N for every group that remains in the result. The more groups there are, the higher &lt;code&gt;N&lt;/code&gt; is, and the more expressions you use, the more memory is required. The size of the values and sort keys also affects memory usage, so it is best not to set an unnecessarily high limit "just in case".&lt;/p&gt;

&lt;p&gt;Full documentation for the new functionality &lt;a href="https://manual.manticoresearch.com/Searching/Grouping#GROUP_CONCAT%28field%29" rel="noopener noreferrer"&gt;is available here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More examples
&lt;/h2&gt;

&lt;p&gt;Support events are just one possible scenario. The new mode can be useful anywhere a document already contains a group key, the value you want to collect, and a field to sort by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product images:&lt;/strong&gt; collect the first few IDs or paths for each &lt;code&gt;product_id&lt;/code&gt;, sorted by &lt;code&gt;display_order&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority tasks:&lt;/strong&gt; return up to six task IDs for each &lt;code&gt;assignee_id&lt;/code&gt;, sorted by a precomputed &lt;code&gt;priority&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server errors:&lt;/strong&gt; show the latest N errors for each server while keeping their total count separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need a short list of IDs, names, or paths, &lt;code&gt;GROUP_CONCAT(... ORDER BY ... LIMIT N)&lt;/code&gt; now lets you get it in a single query. We hope you find this useful.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to search for long hashes and IDs with dict='keywords_32k'</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Wed, 02 Sep 2026 07:30:01 +0000</pubDate>
      <link>https://dev.to/sanikolaev/how-to-search-for-long-hashes-and-ids-with-dictkeywords32k-oo2</link>
      <guid>https://dev.to/sanikolaev/how-to-search-for-long-hashes-and-ids-with-dictkeywords32k-oo2</guid>
      <description>&lt;p&gt;Originally posted by Manticore Search on &lt;a href="https://manticoresearch.com/blog/dict_keywords_32k/" rel="noopener noreferrer"&gt;https://manticoresearch.com/blog/dict_keywords_32k/&lt;/a&gt; on August 31, 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  How to search for long hashes and IDs with dict='keywords_32k'
&lt;/h1&gt;

&lt;p&gt;A practical guide to searching long hashes, event IDs, message IDs, and email addresses in Manticore Search: limits, exact matching, wildcard search, tokenization, migration, and limitations.&lt;/p&gt;

&lt;p&gt;Full-text search usually works with ordinary words: product names, titles, comments, and descriptions. Such tokens are rarely longer than a few dozen characters.&lt;/p&gt;

&lt;p&gt;Logs and technical data are different. A SHA-256 hash is 64 characters long, while message IDs, correlation keys, event identifiers, and some email addresses can be even longer. The value is often meaningful only as a whole: if its tail is lost, one ID can easily be confused with another.&lt;/p&gt;

&lt;p&gt;Manticore Search provides &lt;code&gt;dict='keywords_32k'&lt;/code&gt; for these cases.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;keywords_32k&lt;/code&gt; is available starting with Manticore Search 27.1.1. Version 27.1.5 or newer is recommended when converting an existing table from &lt;code&gt;keywords&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem with the regular dictionary
&lt;/h2&gt;

&lt;p&gt;By default, Manticore uses &lt;code&gt;dict='keywords'&lt;/code&gt;. Its maximum token length is &lt;strong&gt;42 bytes after normalization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The limit is measured in bytes, not characters. One ASCII character takes one byte, but a UTF-8 character can take several bytes.&lt;/p&gt;

&lt;p&gt;If a token exceeds 42 bytes, Manticore truncates it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when indexing a document;&lt;/li&gt;
&lt;li&gt;when processing a search query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result, querying the complete long value does not necessarily return zero results. Because the query is truncated too, the document may be found-but only by the first 42 bytes.&lt;/p&gt;

&lt;p&gt;This creates a more serious problem: two different IDs with the same first 42 bytes become indistinguishable to full-text search. It is also impossible to find a value by a fragment located after the 42nd byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;keywords_32k&lt;/code&gt; changes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dict='keywords_32k'&lt;/code&gt; increases the maximum normalized token length to &lt;strong&gt;32768 bytes&lt;/strong&gt;, or 32 KB.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;&lt;code&gt;dict='keywords'&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;dict='keywords_32k'&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Maximum token length&lt;/td&gt;
&lt;td&gt;42 bytes&lt;/td&gt;
&lt;td&gt;32768 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token exceeding the limit&lt;/td&gt;
&lt;td&gt;Truncated&lt;/td&gt;
&lt;td&gt;Skipped with a warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix and infix search&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Morphology for tokens longer than 42 bytes&lt;/td&gt;
&lt;td&gt;Token is already truncated&lt;/td&gt;
&lt;td&gt;Not applied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RT tables&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain tables&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The setting applies to the entire table:&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;events&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;event_id&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;dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'keywords_32k'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regular short words in the same table continue to use the configured morphology. Tokens longer than 42 bytes are stored in their original normalized form, without stemming or lemmatization.&lt;/p&gt;

&lt;p&gt;For machine identifiers, this is usually exactly what you need: a hash or event ID has no useful word stem.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use &lt;code&gt;keywords_32k&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Use it when both conditions are true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The value can exceed 42 bytes after tokenization.&lt;/li&gt;
&lt;li&gt;It must be searchable through &lt;code&gt;MATCH()&lt;/code&gt;, by prefix, or by substring.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Typical examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SHA-256 and other long hashes;&lt;/li&gt;
&lt;li&gt;event IDs and message IDs;&lt;/li&gt;
&lt;li&gt;request IDs, trace IDs, and other technical identifiers;&lt;/li&gt;
&lt;li&gt;long record keys;&lt;/li&gt;
&lt;li&gt;email addresses with a long local or domain part;&lt;/li&gt;
&lt;li&gt;technical values from logs;&lt;/li&gt;
&lt;li&gt;long identifiers containing separators.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, &lt;code&gt;keywords_32k&lt;/code&gt; is not required for every ID lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you only need exact equality
&lt;/h3&gt;

&lt;p&gt;If the application always receives the complete ID and only needs to check exact equality, a string attribute is sufficient:&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;events&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;event_id&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use a regular 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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s1"&gt;'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  If you need both exact matching and full-text search
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;string attribute indexed&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;events&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;event_id&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'keywords_32k'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, Manticore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stores the original value as a string attribute;&lt;/li&gt;
&lt;li&gt;lets you filter it with &lt;code&gt;WHERE&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;also indexes it for &lt;code&gt;MATCH()&lt;/code&gt; and wildcard search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is usually the most convenient schema for technical identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching by the complete token
&lt;/h2&gt;

&lt;p&gt;Create a table and add a 64-character SHA-256 hash:&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;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;events&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;TABLE&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;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;event_id&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'keywords_32k'&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;events&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;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&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;'delivery accepted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the complete normalized token in a full-text search:&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&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;'@event_id 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'&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;@event_id&lt;/code&gt; operator restricts the search to the required field. Without it, Manticore searches for the value in all full-text fields of the table.&lt;/p&gt;

&lt;p&gt;Quotes are not needed around a single simple alphanumeric token here. Quotes denote a phrase search; they do not turn &lt;code&gt;MATCH()&lt;/code&gt; into a byte-for-byte comparison of the original string.&lt;/p&gt;

&lt;h2&gt;
  
  
  A full-text match is not the same as exact equality
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MATCH()&lt;/code&gt; operates on the result of tokenization and normalization. It can be affected by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;charset_table&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;conversion to lowercase;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;blend_chars&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ignore_chars&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;word forms and other text-processing settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a strict comparison of the stored value, use a string attribute:&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;collation_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'binary'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&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;message&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s1"&gt;'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'&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;binary&lt;/code&gt; enables byte-for-byte string comparisons in the current SQL session. This setting does not affect full-text search behavior.&lt;/p&gt;

&lt;p&gt;The practical rule is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WHERE event_id = ...&lt;/code&gt; - a strict comparison of the stored string;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MATCH('@event_id ...')&lt;/code&gt; - a normalized-token search;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MATCH('@event_id prefix*')&lt;/code&gt; - a prefix search;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MATCH('@event_id *fragment*')&lt;/code&gt; - a substring search.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to inspect tokenization
&lt;/h2&gt;

&lt;p&gt;Before loading a large volume of data, check that Manticore actually sees the value as a single 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;CALL&lt;/span&gt; &lt;span class="n"&gt;KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'events'&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;normalized&lt;/code&gt; column should contain the complete 64-character hash.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CALL KEYWORDS&lt;/code&gt; is especially useful for values containing periods, hyphens, &lt;code&gt;@&lt;/code&gt;, colons, slashes, and characters from different writing systems. This lets you inspect the actual token boundaries before indexing the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefix and substring search
&lt;/h2&gt;

&lt;p&gt;To search inside a token, enable &lt;code&gt;min_infix_len&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;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;events_infix&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_infix&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;event_id&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'keywords_32k'&lt;/span&gt;
&lt;span class="n"&gt;min_infix_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'4'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefix search:&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_infix&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;'@event_id 9f86d081*'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Substring search:&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_infix&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;'@event_id *b2b0b822*'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A positive &lt;code&gt;min_infix_len&lt;/code&gt; also enables prefix search. This example uses &lt;code&gt;4&lt;/code&gt; to prevent excessively short and broad patterns.&lt;/p&gt;

&lt;p&gt;For production systems: do not allow excessively short fragments, choose &lt;code&gt;min_infix_len&lt;/code&gt; based on real data, use &lt;code&gt;expansion_limit&lt;/code&gt;, test performance with a production-sized dictionary, and restrict search to a specific field with &lt;code&gt;@field&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email addresses and other values with separators
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;keywords_32k&lt;/code&gt; changes only the maximum token length. It does not determine where a token begins and ends.&lt;/p&gt;

&lt;p&gt;By default, a period, &lt;code&gt;@&lt;/code&gt;, hyphen, and other characters may split a value into multiple parts. If an email address or message ID must also be indexed as a whole, you can use &lt;code&gt;blend_chars&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;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;mail_events&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;mail_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subject&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;dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'keywords_32k'&lt;/span&gt;
&lt;span class="n"&gt;blend_chars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'., @, -'&lt;/span&gt;
&lt;span class="n"&gt;min_infix_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'4'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blended characters are indexed in two ways: as part of the complete token and as separators between regular parts of the value. This makes it possible to search both the complete email address and individual words within it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to convert an existing table
&lt;/h2&gt;

&lt;p&gt;For an RT table, you can change the setting with &lt;code&gt;ALTER 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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'keywords_32k'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this affects only documents added or replaced after the setting is changed. Existing documents are not automatically tokenized again. Their long tokens remain in the old truncated form until the documents are reindexed.&lt;/p&gt;

&lt;p&gt;Follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update &lt;code&gt;dict&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify the setting with &lt;code&gt;SHOW CREATE TABLE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reindex or reload the existing documents.&lt;/li&gt;
&lt;li&gt;Check several long values with &lt;code&gt;CALL KEYWORDS&lt;/code&gt; and &lt;code&gt;MATCH()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;dict='crc'&lt;/code&gt; does not solve this problem
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dict='crc'&lt;/code&gt; stores keyword checksums instead of their original text. However, it does not increase the allowed token length. The exception to the regular 42-byte limit is implemented specifically by &lt;code&gt;dict='keywords_32k'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;p&gt;At the time of publication, &lt;code&gt;dict='keywords_32k'&lt;/code&gt; has several limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CALL SUGGEST&lt;/code&gt; and &lt;code&gt;CALL QSUGGEST&lt;/code&gt; are not supported;&lt;/li&gt;
&lt;li&gt;it cannot be used in percolate tables;&lt;/li&gt;
&lt;li&gt;tokens longer than 42 bytes are not highlighted in snippets or highlights;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;indextool --dumpdict&lt;/code&gt; cannot dump this type of dictionary;&lt;/li&gt;
&lt;li&gt;the full-text &lt;code&gt;REGEX&lt;/code&gt; operator works with &lt;code&gt;dict='keywords'&lt;/code&gt;, but not with &lt;code&gt;keywords_32k&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Do not index secrets
&lt;/h2&gt;

&lt;p&gt;Being able to search a long value does not mean that you should store it in a search index. Do not index API keys, bearer tokens, session cookies, private keys, passwords, reset tokens, or other data that grants access to the system unless absolutely necessary.&lt;/p&gt;

&lt;p&gt;If a secret must be matched by its exact value, it is safer to calculate a suitable hash in advance and store only the hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;p&gt;Before enabling &lt;code&gt;keywords_32k&lt;/code&gt;, check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the normalized token actually longer than 42 bytes?&lt;/li&gt;
&lt;li&gt;Do you need full-text or wildcard search rather than only &lt;code&gt;WHERE value = ...&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;CALL KEYWORDS&lt;/code&gt; see the entire value as one token?&lt;/li&gt;
&lt;li&gt;Do you need &lt;code&gt;blend_chars&lt;/code&gt; for periods, hyphens, &lt;code&gt;@&lt;/code&gt;, and other separators?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;min_infix_len&lt;/code&gt; large enough?&lt;/li&gt;
&lt;li&gt;Is the number of wildcard expansions limited?&lt;/li&gt;
&lt;li&gt;Have the old documents been reindexed?&lt;/li&gt;
&lt;li&gt;Is the field free of secret data?&lt;/li&gt;
&lt;li&gt;Does the application avoid relying on highlighting, &lt;code&gt;SUGGEST&lt;/code&gt;, percolate, or full-text &lt;code&gt;REGEX&lt;/code&gt;?&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;code&gt;dict='keywords_32k'&lt;/code&gt; solves a specific problem: it allows a full-text index to store normalized tokens up to 32768 bytes long instead of the regular 42 bytes.&lt;/p&gt;

&lt;p&gt;It is well suited to long hashes, event IDs, message IDs, email addresses, and other machine identifiers. Keep three points in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;keywords_32k&lt;/code&gt; increases the token length but does not change tokenization rules;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MATCH()&lt;/code&gt; on a complete token is not the same as a strict comparison of the original string;&lt;/li&gt;
&lt;li&gt;existing documents must be reindexed after the setting is changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only need exact equality, use a string attribute. If you need both exact matching and partial-value search, use &lt;code&gt;string attribute indexed&lt;/code&gt; together with &lt;code&gt;dict='keywords_32k'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&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&lt;/code&gt; and &lt;code&gt;keywords_32k&lt;/code&gt; limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Data_tokenization#Token-length-limit" rel="noopener noreferrer"&gt;Token length limit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Wildcard_searching_settings" rel="noopener noreferrer"&gt;Wildcard search settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Low-level_tokenization#blend_chars" rel="noopener noreferrer"&gt;&lt;code&gt;blend_chars&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Searching/Autocomplete#CALL-KEYWORDS" rel="noopener noreferrer"&gt;&lt;code&gt;CALL KEYWORDS&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Creating_a_table/Local_tables#String" rel="noopener noreferrer"&gt;String attributes and indexed strings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Updating_table_schema_and_settings" rel="noopener noreferrer"&gt;Updating full-text settings and reindexing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Searching/Collations" rel="noopener noreferrer"&gt;Collations and string comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manticoresearch.com/blog/manticore-search-27-1-5/" rel="noopener noreferrer"&gt;Manticore Search 27.1.5&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>search</category>
      <category>sql</category>
      <category>performance</category>
    </item>
    <item>
      <title>How KupujemProdajem searches 5.6 million ads at ~10 ms with Manticore Search</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Mon, 31 Aug 2026 07:00:01 +0000</pubDate>
      <link>https://dev.to/sanikolaev/how-kupujemprodajem-searches-56-million-ads-at-10-ms-with-manticore-search-1373</link>
      <guid>https://dev.to/sanikolaev/how-kupujemprodajem-searches-56-million-ads-at-10-ms-with-manticore-search-1373</guid>
      <description>&lt;p&gt;Originally posted by Manticore Search on &lt;a href="https://manticoresearch.com/blog/kupujemprodajem/" rel="noopener noreferrer"&gt;https://manticoresearch.com/blog/kupujemprodajem/&lt;/a&gt; on August 31, 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  How KupujemProdajem searches 5.6 million ads at ~10 ms with Manticore Search
&lt;/h1&gt;

&lt;p&gt;How Serbia's largest online classifieds marketplace uses Manticore Search to search 5.6 million active ads at ~10 ms, process real-time updates, and prepares for hybrid search.&lt;/p&gt;

&lt;p&gt;For an online marketplace, having millions of listings is useful only if buyers can find the right one.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.kupujemprodajem.com/" rel="noopener noreferrer"&gt;KupujemProdajem&lt;/a&gt;, Serbia's largest online classifieds marketplace, search is the primary way users discover ads. Buyers search from the web and mobile applications, while internal teams use search for moderation and support. Because of that, both relevance and latency directly affect user engagement and sellers' ability to be discovered.&lt;/p&gt;

&lt;p&gt;Today, KupujemProdajem uses Manticore Search to search approximately &lt;strong&gt;5.6 million active ads&lt;/strong&gt;, while separate administrative indexes contain around &lt;strong&gt;70 million rows&lt;/strong&gt; of current and historical data. The production cluster handles around &lt;strong&gt;150 search requests per second&lt;/strong&gt; on average, with an average Manticore query time of approximately &lt;strong&gt;10 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And the same search infrastructure is now giving the team a path toward something more ambitious: &lt;a href="https://manticoresearch.com/blog/hybrid-search/" rel="noopener noreferrer"&gt;hybrid lexical and semantic search&lt;/a&gt; designed specifically for Serbian-language marketplace content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search is part of the core product
&lt;/h2&gt;

&lt;p&gt;KupujemProdajem maintains two main kinds of search indexes. The first powers the customer-facing experience. It contains approximately 5.6 million active classified ads, including titles, descriptions, categories, and structured attributes. Users search these indexes while browsing the marketplace on web and mobile.&lt;/p&gt;

&lt;p&gt;The second set of indexes is used internally. In addition to current data, these indexes retain part of the history of deleted ads, allowing moderation and support teams to search information that may no longer be visible on the public marketplace.&lt;/p&gt;

&lt;p&gt;This makes search important on both sides of the product. For buyers, it determines how quickly they can get from an idea of what they want to a relevant listing. For sellers, search affects whether their ads are discovered. Internally, it gives employees access to historical marketplace data when investigating problems.&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://www.linkedin.com/in/dimitrije-marinkovi%C4%87-131bb62bb/" rel="noopener noreferrer"&gt;Dimitrije Marinković&lt;/a&gt;, Backend Developer at KupujemProdajem, puts it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Search is the primary discovery mechanism on the platform, so relevance and latency directly affect user engagement and seller success.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Choosing a search engine that could grow with the product
&lt;/h2&gt;

&lt;p&gt;KupujemProdajem had already been running a dedicated search engine for years. When its previous engine, Sphinx, stopped being actively developed, the team needed a maintained alternative that would fit the existing architecture without requiring the entire search integration to be rebuilt.&lt;/p&gt;

&lt;p&gt;But compatibility was only part of the decision. The new engine also needed to support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consistently low query latency;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manual.manticoresearch.com/Creating_a_table/Local_tables/Real-time_table" rel="noopener noreferrer"&gt;real-time changes&lt;/a&gt; to millions of ads;&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;replication&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manual.manticoresearch.com/Connecting_to_the_server/MySQL_protocol" rel="noopener noreferrer"&gt;SQL&lt;/a&gt; that worked naturally with the team's MySQL-centric PHP stack;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manual.manticoresearch.com/Searching/Full_text_matching/Basic_usage" rel="noopener noreferrer"&gt;full-text search&lt;/a&gt; together with &lt;a href="https://manual.manticoresearch.com/Searching/Filters" rel="noopener noreferrer"&gt;structured filters&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;flexible &lt;a href="https://manual.manticoresearch.com/Searching/Options#ranker" rel="noopener noreferrer"&gt;relevance tuning&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;and a path toward &lt;a href="https://manticoresearch.com/blog/vector-search-deep-dive/" rel="noopener noreferrer"&gt;vector&lt;/a&gt; and &lt;a href="https://manual.manticoresearch.com/Searching/Hybrid_search" rel="noopener noreferrer"&gt;hybrid search&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manticore provided those capabilities in one system. That last requirement is becoming increasingly important.&lt;/p&gt;

&lt;p&gt;KupujemProdajem plans to combine traditional lexical search with vector search using its own &lt;a href="https://manticoresearch.com/blog/vector-search-in-databases/#embeddings" rel="noopener noreferrer"&gt;embedding model&lt;/a&gt;. Because Manticore supports &lt;a href="https://manual.manticoresearch.com/Searching/KNN" rel="noopener noreferrer"&gt;KNN search&lt;/a&gt; alongside full-text search, the team can build that functionality inside the search infrastructure it already operates instead of synchronizing its data with a separate vector database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping millions of changing ads searchable in real time
&lt;/h2&gt;

&lt;p&gt;MySQL remains the source of truth for marketplace data. When an ad is created, edited, or removed, the corresponding change is pushed to Manticore in real time. The production workload averages approximately &lt;strong&gt;40 &lt;a href="https://manual.manticoresearch.com/Data_creation_and_modification/Updating_documents/REPLACE" rel="noopener noreferrer"&gt;&lt;code&gt;REPLACE&lt;/code&gt;&lt;/a&gt; operations per second&lt;/strong&gt;, with additional &lt;a href="https://manual.manticoresearch.com/Data_creation_and_modification/Updating_documents/UPDATE" rel="noopener noreferrer"&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://manual.manticoresearch.com/Data_creation_and_modification/Deleting_documents" rel="noopener noreferrer"&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/a&gt; operations.&lt;/p&gt;

&lt;p&gt;This is particularly useful for classifieds. Marketplace inventory changes continuously. Sellers publish new ads, update prices and descriptions, change status, and remove products that are no longer available.&lt;/p&gt;

&lt;p&gt;A search system therefore has two jobs at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;return relevant results quickly;&lt;/li&gt;
&lt;li&gt;make sure those results reflect the current marketplace.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;KupujemProdajem uses Manticore's real-time updates to keep the searchable representation of the marketplace synchronized with MySQL as these changes happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than matching words
&lt;/h2&gt;

&lt;p&gt;Marketplace search also has very different requirements from searching a collection of plain documents. A listing contains text, but it also has structured information such as category, price, location, status, and other properties. KupujemProdajem combines full-text matching with filters over these typed attributes in Manticore.&lt;/p&gt;

&lt;p&gt;The team also controls how different parts of the listing affect relevance. The title, for example, receives significantly more weight than the description or category. This makes intuitive sense for classified ads: a product named directly in the title is normally a stronger signal than the same word appearing somewhere in a longer description.&lt;/p&gt;

&lt;p&gt;On top of this, KupujemProdajem uses a &lt;a href="https://manual.manticoresearch.com/Searching/Sorting_and_ranking#Ranking-overview" rel="noopener noreferrer"&gt;custom ranker&lt;/a&gt; tuned for its relevance requirements and &lt;a href="https://manual.manticoresearch.com/Searching/Grouping" rel="noopener noreferrer"&gt;groups results&lt;/a&gt; by ad. This gives the team the ability to encode marketplace-specific relevance rules rather than relying entirely on a generic ranking formula.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small replicated cluster
&lt;/h2&gt;

&lt;p&gt;The entire Manticore deployment runs on four virtual machines. Three nodes form the production replication cluster, while a fourth VM serves as a backup. All writes are sent to one node and replicated to the others. Read requests are distributed among all three production nodes by application-level logic.&lt;/p&gt;

&lt;p&gt;The architecture is relatively simple: &lt;strong&gt;MySQL → real-time updates → Manticore write node → replicated Manticore nodes → web/mobile/admin search&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That simplicity matters operationally. The team does not need a large distributed data platform merely to support marketplace search. The same cluster handles ingestion, full-text retrieval, structured filtering, custom ranking, replication, and the foundation for future vector search.&lt;/p&gt;

&lt;h2&gt;
  
  
  5.6 million active ads, 70 million admin rows
&lt;/h2&gt;

&lt;p&gt;The user-facing indexes currently contain around &lt;strong&gt;5.6 million active ads&lt;/strong&gt; and occupy approximately &lt;strong&gt;9 GB&lt;/strong&gt;. The larger administrative indexes contain approximately &lt;strong&gt;70 million rows&lt;/strong&gt;, including some historical deleted-ad data, and occupy around &lt;strong&gt;90 GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Across the three-node cluster, KupujemProdajem sees approximately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Production scale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Active ads&lt;/td&gt;
&lt;td&gt;~5.6 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin index rows&lt;/td&gt;
&lt;td&gt;~70 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-facing index size&lt;/td&gt;
&lt;td&gt;~9 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin index size&lt;/td&gt;
&lt;td&gt;~90 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search traffic&lt;/td&gt;
&lt;td&gt;~150 queries/sec average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search traffic per node&lt;/td&gt;
&lt;td&gt;~50 queries/sec average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;REPLACE&lt;/code&gt; traffic&lt;/td&gt;
&lt;td&gt;~40/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average query time&lt;/td&gt;
&lt;td&gt;~10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;3 production VMs + 1 backup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cluster remains stable under normal operation, without stuck or long-running queries. Short latency spikes of approximately 150 ms occur occasionally, while the average query execution time remains around 10 ms. Those numbers are particularly useful because this is not a synthetic benchmark. They describe an actual production workload behind the primary discovery experience of a large marketplace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What users get from this architecture
&lt;/h2&gt;

&lt;p&gt;Infrastructure metrics matter, but only because of what they enable at the product level. For buyers, Manticore helps KupujemProdajem keep search fast even while querying millions of listings and applying relevance logic and structured filters. Because changes are continuously pushed into the index, search can also reflect changes in marketplace inventory quickly instead of relying on occasional large reindexing jobs.&lt;/p&gt;

&lt;p&gt;For sellers, this infrastructure supports the main mechanism through which buyers discover their listings. For moderation and support teams, the much larger internal indexes make historical ad information searchable even when it is no longer part of the live marketplace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next challenge: understanding Serbian-language intent
&lt;/h2&gt;

&lt;p&gt;The next project for the KupujemProdajem team is &lt;a href="https://manticoresearch.com/blog/hybrid-search/" rel="noopener noreferrer"&gt;hybrid search&lt;/a&gt;. Traditional full-text search works particularly well when the words entered by a user also appear in an ad. But marketplace users and sellers do not always describe the same thing in exactly the same way.&lt;/p&gt;

&lt;p&gt;That problem becomes even more interesting in Serbian. The team highlights several challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rich &lt;a href="https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Morphology" rel="noopener noreferrer"&gt;morphology&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;both Latin and Cyrillic scripts;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Wordforms" rel="noopener noreferrer"&gt;synonyms&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;different ways buyers and sellers can express the same intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;KupujemProdajem is therefore preparing its own embedding model for Serbian marketplace content. The plan is to retrieve results using both &lt;a href="https://manticoresearch.com/use-case/lexical-search/" rel="noopener noreferrer"&gt;lexical matching&lt;/a&gt; and KNN vector search, then combine the two sets of signals at query time.&lt;/p&gt;

&lt;p&gt;The user benefit is straightforward. Someone may search for an item using words that never literally appear in the seller's listing, while the listing is nevertheless highly relevant. Semantic retrieval can provide another signal for recognizing that relationship.&lt;/p&gt;

&lt;p&gt;As Dimitrije describes the expected result:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Semantic matching will let users find relevant ads even when their wording doesn't literally match the listing.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perhaps just as important for the engineering team, they do not need to introduce another database to do it. The vector representation can live alongside the text and structured attributes already searchable in Manticore, giving the team one engine in which to combine lexical relevance, filters, and semantic similarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  A foundation for better marketplace search
&lt;/h2&gt;

&lt;p&gt;KupujemProdajem already uses Manticore for one of the most important workflows on its marketplace. Approximately &lt;strong&gt;5.6 million active ads&lt;/strong&gt; are searchable with an average engine query time of &lt;strong&gt;around 10 ms&lt;/strong&gt;. Updates arrive continuously. Replication provides multiple search nodes. Internal teams search a much larger historical dataset using the same technology.&lt;/p&gt;

&lt;p&gt;But the more interesting part may be what comes next. The team can now experiment with semantic retrieval and hybrid relevance without replacing the search infrastructure or operating an additional vector database.&lt;/p&gt;

&lt;p&gt;For KupujemProdajem, that means Manticore is not only keeping today's marketplace search fast. It is also giving the team a practical way to make tomorrow's search better.&lt;/p&gt;

</description>
      <category>database</category>
      <category>search</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Full-Text Search Still Works. It Just Doesn’t Get You to an Answer</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Mon, 24 Aug 2026 10:34:39 +0000</pubDate>
      <link>https://dev.to/sanikolaev/full-text-search-still-works-it-just-doesnt-get-you-to-an-answer-1dhd</link>
      <guid>https://dev.to/sanikolaev/full-text-search-still-works-it-just-doesnt-get-you-to-an-answer-1dhd</guid>
      <description>&lt;p&gt;Originally posted by Klim Todrik on &lt;a href="https://manticoresearch.com/blog/conversational-search/" rel="noopener noreferrer"&gt;https://manticoresearch.com/blog/conversational-search/&lt;/a&gt; on Aug 20, 2026&lt;/p&gt;

&lt;p&gt;Imagine a typical online shoe store.&lt;/p&gt;

&lt;p&gt;A shopper opens search and types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I need black waterproof running shoes for daily runs on wet pavement. What would you recommend?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few years ago, almost no one expected this from a search box. The query would have been shortened to something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;black waterproof running shoes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the shopper would open several product cards, compare descriptions, materials, intended use, and price, and make the decision alone.&lt;/p&gt;

&lt;p&gt;Today, people increasingly expect search itself to do part of that work. Not because full-text search has become worse. It still performs very well with exact names, SKUs, product codes, brands, and keywords. What has changed is what people expect to be able to ask a search system.&lt;/p&gt;

&lt;p&gt;For example, Google reported in May 2026 that AI Mode had surpassed one billion monthly users. The company notes that people are asking longer, more complex questions that previously did not fit into conventional search. (&lt;a href="https://blog.google/products-and-platforms/products/search/search-io-2026/" rel="noopener noreferrer"&gt;blog.google&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The same thing happens in an online store.&lt;/p&gt;

&lt;p&gt;A query such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I need black waterproof running shoes for daily runs on wet pavement.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only looks like one sentence. For a search system, it contains several tasks.&lt;/p&gt;

&lt;p&gt;It needs to extract constraints such as color and waterproofing; understand that this is about running rather than walking; account for price, size, and availability; find suitable products; and, if the user asks “which are better,” explain the differences.&lt;/p&gt;

&lt;p&gt;No single algorithm solves all of that.&lt;/p&gt;

&lt;p&gt;Full-text search, vector search, filters, hybrid search, and a language model each solve different parts of the problem. It is far more effective to combine them than to choose between them.&lt;/p&gt;

&lt;p&gt;That is exactly why Manticore has Conversational Search.&lt;/p&gt;




&lt;h2&gt;
  
  
  Word search, semantic search, and conversation are different tasks
&lt;/h2&gt;

&lt;p&gt;Start with a simple query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nike Pegasus 41 black
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the system barely needs to interpret the user’s intent. Full-text search handles it directly.&lt;/p&gt;

&lt;p&gt;Or something even simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKU 123456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Semantic methods are not needed here.&lt;/p&gt;

&lt;p&gt;Now consider another example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;light shoes for long summer walks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A product card may not contain the words “summer” or “long walks,” but it may include details such as “breathable material” or “lightweight construction.”&lt;/p&gt;

&lt;p&gt;This is where vector search becomes useful.&lt;/p&gt;

&lt;p&gt;Real queries often fall between these extremes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;black Gore-Tex shoes for everyday running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some parameters — &lt;code&gt;black&lt;/code&gt; and &lt;code&gt;Gore-Tex&lt;/code&gt; — need to be preserved. &lt;code&gt;Everyday running&lt;/code&gt; describes the user’s intent rather than an exact attribute.&lt;/p&gt;

&lt;p&gt;For such cases, Manticore uses hybrid search, combining full-text and vector search through result ranking.&lt;/p&gt;

&lt;p&gt;But even hybrid search returns only a list of results.&lt;/p&gt;

&lt;p&gt;At that point, search considers its job done. The user usually does not.&lt;/p&gt;

&lt;p&gt;It does not answer questions such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which of these models are better suited to rain?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it certainly does not handle a follow-up such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which of those cost less than $120?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a conversation. It needs another layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;To test this in practice, we used &lt;a href="https://arxiv.org/abs/2602.16938" rel="noopener noreferrer"&gt;ConvApparel&lt;/a&gt;, a dataset of conversations about choosing apparel. After cleanup, it contained 82,524 products: footwear, pants, tops, and outerwear. Each product has a description, category, images, and attributes. We built Manticore Apparel Shop on this data.&lt;/p&gt;

&lt;p&gt;For example, you can type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I need black waterproof running shoes for jogging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system first finds suitable products, then a language model generates an answer using them as context, while the interface shows the products themselves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Try the demo:&lt;/strong&gt; &lt;a href="https://chat.manticoresearch.com/" rel="noopener noreferrer"&gt;Manticore Apparel Shop&lt;/a&gt; generates a random product and a query that should retrieve it, then demonstrates that the same query does retrieve that product through Manticore.&lt;/p&gt;
&lt;/blockquote&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%2Fvj2udfja5lxjw43kyxbq.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%2Fvj2udfja5lxjw43kyxbq.png" alt="Conversational Search shows an answer and its source products in Manticore Apparel Shop" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is important to keep the connection between the answer and the data. If the system claims that a model is suitable for rain, the user should be able to open the product and verify the source of that claim.&lt;/p&gt;

&lt;p&gt;In this approach, the language model does not replace search. It interprets its results.&lt;/p&gt;




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

&lt;p&gt;Two main commands are used:&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="n"&gt;CHAT&lt;/span&gt; &lt;span class="n"&gt;MODEL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&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;CALL&lt;/span&gt; &lt;span class="n"&gt;CHAT&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, you create a Conversational Search model and set the rules it follows.&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="n"&gt;CHAT&lt;/span&gt; &lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'openrouter:google/gemma-4-26b-a4b-it'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retrieval_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_document_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;custom_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'You are a context-only shopping assistant.

Answer using only the provided context.
Do not use outside knowledge or unsupported assumptions.

Recommend only products supported by the retrieved context.
For every recommended product, briefly explain why it matches the request.

End every recommendation with the corresponding
context source ID in the format [ref:&amp;lt;id&amp;gt;].

If none of the retrieved products support the request,
say that you do not have enough information.'&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;retrieval_limit&lt;/code&gt; determines how many documents enter the context. &lt;code&gt;max_document_length&lt;/code&gt; limits the amount of text from each document.&lt;/p&gt;

&lt;p&gt;If there is too little context, the model will not see the right products. If there is too much, latency and query cost increase. Like a person, a language model does not become smarter just because it has been given everything to read.&lt;/p&gt;




&lt;p&gt;You can then run a 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;CALL&lt;/span&gt; &lt;span class="n"&gt;CHAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'I need black waterproof running shoes for jogging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'convapparel_products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'assistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'demo-session-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'embedding_vector'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then continue the conversation:&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;CALL&lt;/span&gt; &lt;span class="n"&gt;CHAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Which of these are better for daily use?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'convapparel_products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'assistant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'demo-session-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'embedding_vector'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system uses conversation history, so the user does not need to repeat the context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Through the HTTP API
&lt;/h2&gt;

&lt;p&gt;Conversational Search is also available through the JSON API:&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;"chat"&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I need black waterproof running shoes for jogging"&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;"convapparel_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;"model_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"conversation_uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-session-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vector_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;"embedding_vector"&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;The request is sent to &lt;code&gt;/search&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the system returns
&lt;/h2&gt;

&lt;p&gt;The response contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;conversation_uuid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user_query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_query&lt;/code&gt; — the search query generated by the system&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sources&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;search_query&lt;/code&gt; is particularly important.&lt;/p&gt;

&lt;p&gt;If the user writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which of these would work better in rain?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On its own, this query makes no sense without context. The system therefore forms a complete search query using the conversation history.&lt;/p&gt;

&lt;p&gt;This also simplifies debugging: you can trace the entire chain from query to answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  How retrieval works
&lt;/h2&gt;

&lt;p&gt;Conversational Search uses vector search over an embedding field. The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user question
→ conversation history
→ search query
→ vector search
→ retrieved documents
→ language model
→ answer and sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where search ends and conversation begins
&lt;/h2&gt;

&lt;p&gt;Full-text search works well for exact queries. Vector search works with semantic ones. Hybrid search works with their combination. Conversational Search is needed when the result must be explained, compared, or refined.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quality
&lt;/h2&gt;

&lt;p&gt;We tested this on our &lt;a href="https://github.com/manticoresoftware/conversational-search-quality-becnhmark" rel="noopener noreferrer"&gt;conversational search quality benchmark&lt;/a&gt;, using 200 deterministic shopping queries from ConvApparel. The benchmark evaluates the product IDs returned as sources, rather than the wording of the generated answer.&lt;/p&gt;

&lt;p&gt;In the current run, Manticore scored &lt;strong&gt;0.3650 Hit@3&lt;/strong&gt;, &lt;strong&gt;0.4250 Hit@5&lt;/strong&gt;, &lt;strong&gt;0.5250 Hit@10&lt;/strong&gt;, and &lt;strong&gt;0.2790 MRR&lt;/strong&gt; — the best result on each of those metrics among the engines tested. The full repository includes the dataset-building rules, engine configuration, smoke tasks, and raw results.&lt;/p&gt;




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

&lt;p&gt;Search in modern systems is not one algorithm but several layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full-text search&lt;/li&gt;
&lt;li&gt;vector search&lt;/li&gt;
&lt;li&gt;hybrid search&lt;/li&gt;
&lt;li&gt;Conversational Search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each solves its own task. A language model does not replace search; it works on top of it.&lt;/p&gt;

&lt;p&gt;Without good search, you do not get a smart assistant; you get a very talkative consultant that barely knows its own catalog.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Want to test the approach in practice?&lt;/strong&gt; Try &lt;a href="https://chat.manticoresearch.com/" rel="noopener noreferrer"&gt;Manticore Apparel Shop&lt;/a&gt;: choose a random product, ask a question based on it, and confirm that the same product is found and suggested in the answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to run it locally or explore the code, check the &lt;a href="https://github.com/manticoresoftware/demo-conversational-search" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>database</category>
      <category>search</category>
    </item>
    <item>
      <title>UUID in Manticore: A Practical Guide</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Thu, 06 Aug 2026 05:20:04 +0000</pubDate>
      <link>https://dev.to/sanikolaev/uuid-in-manticore-a-practical-guide-2foc</link>
      <guid>https://dev.to/sanikolaev/uuid-in-manticore-a-practical-guide-2foc</guid>
      <description>&lt;p&gt;In the &lt;a href="https://manticoresearch.com/blog/uuid-document-ids/" rel="noopener noreferrer"&gt;overview article&lt;/a&gt; we explained why it makes sense to use the same UUID in search and in the primary database, when one already exists. Here we will go straight to practice: create a table, run the core operations through SQL and the JSON API, and then load several documents through &lt;code&gt;/bulk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All examples are for Manticore Search 28.5.0 or later. The &lt;code&gt;&amp;lt;generated UUID&amp;gt;&lt;/code&gt; value in the responses means the UUID that Manticore creates when processing the request. You do not need to copy this string into the next request: substitute the actual &lt;code&gt;id&lt;/code&gt; from your own response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table for all examples
&lt;/h2&gt;

&lt;p&gt;To make UUID the document ID, first declare the field as &lt;code&gt;id uuid&lt;/code&gt;. Aside from the &lt;code&gt;id&lt;/code&gt; type, the RT table schema does not change:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DESC products_uuid&lt;/code&gt; will show that the &lt;code&gt;id&lt;/code&gt; field has the &lt;code&gt;uuid&lt;/code&gt; type. Both SQL and the HTTP API use this value as the document ID, so there is no need to copy the UUID into a string attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with UUID through SQL
&lt;/h2&gt;

&lt;p&gt;First, insert a product with a prebuilt UUID:&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_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;In SQL, the UUID must be enclosed in single quotes. You can find the document with a regular &lt;code&gt;WHERE id = '...'&lt;/code&gt; condition:&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;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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manticore can also generate the UUID. To do that, simply do not pass any &lt;code&gt;id&lt;/code&gt; value:&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_uuid&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;'USB microphone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MIC-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;89&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;LAST_INSERT_ID&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;LAST_INSERT_ID()&lt;/code&gt; will return the UUID created by this request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------------------------------------+
| last_insert_id()                     |
+--------------------------------------+
| &amp;lt;generated UUID&amp;gt;                     |
+--------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;INSERT&lt;/code&gt; for multiple documents and the &lt;code&gt;@@session.last_insert_id&lt;/code&gt; variable are covered in detail in the documentation section on &lt;a href="https://manual.manticoresearch.com/dev/Data_creation_and_modification/Adding_documents_to_a_table/Adding_documents_to_a_real-time_table" rel="noopener noreferrer"&gt;adding documents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's add another document with a known ID to check &lt;code&gt;IN&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;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-446655440001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'USB-C dock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'DOCK-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;119&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&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;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&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;'550e8400-e29b-41d4-a716-446655440001'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can change attributes with a regular &lt;code&gt;UPDATE&lt;/code&gt;. The &lt;code&gt;id&lt;/code&gt; itself stays the same:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;139&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;INSERT&lt;/code&gt; with an already existing UUID will not overwrite the document. Manticore will return a duplicate error:&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_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;'Duplicate keyboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'KB-DUP'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create a new version of the document with the same UUID, use &lt;code&gt;REPLACE&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;REPLACE&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, revised'&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;129&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change only the price, &lt;code&gt;UPDATE&lt;/code&gt; is enough. Full-text fields and columnar attributes require &lt;code&gt;REPLACE&lt;/code&gt;: it marks the old version of the document with the same ID as deleted and writes the new one. If that ID does not exist yet, Manticore simply adds the document. More details are in the documentation: &lt;a href="https://manual.manticoresearch.com/dev/Data_creation_and_modification/Updating_documents/UPDATE" rel="noopener noreferrer"&gt;UPDATE&lt;/a&gt; and &lt;a href="https://manual.manticoresearch.com/dev/Data_creation_and_modification/Updating_documents/REPLACE" rel="noopener noreferrer"&gt;REPLACE&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Delete the document by the same UUID:&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;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'550e8400-e29b-41d4-a716-446655440001'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's check the current state of the document whose UUID we set on insert:&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;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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SQL client sends and receives a UUID as a string. Pass it as a string parameter, and read the &lt;code&gt;id&lt;/code&gt; from &lt;code&gt;SELECT&lt;/code&gt; as a string. There is no need to convert it to a number or &lt;code&gt;BINARY(16)&lt;/code&gt;. Code written for a numeric document ID will need to be adjusted.&lt;/p&gt;

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

&lt;p&gt;When writing through the JSON API, the &lt;code&gt;id&lt;/code&gt; field is passed alongside &lt;code&gt;table&lt;/code&gt;, not inside &lt;code&gt;doc&lt;/code&gt;. In the first example, we use an uppercase UUID:&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; http://localhost:9308/insert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "table": "products_uuid",
    "id": "AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA",
    "doc": {
      "title": "Wireless keyboard",
      "sku": "JSON-KB-001",
      "price": 159
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manticore accepts UUID in uppercase, stores it in lowercase, and returns it lowercased in the 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;"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_uuid"&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="s2"&gt;"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"&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;For automatic generation, remove the &lt;code&gt;id&lt;/code&gt; field entirely:&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; http://localhost:9308/insert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "table": "products_uuid",
    "doc": {
      "title": "Portable speaker",
      "sku": "JSON-SPK-001",
      "price": 79
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response contains the ID that should be saved for subsequent operations:&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;"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_uuid"&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="s2"&gt;"&amp;lt;generated UUID&amp;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;"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;In &lt;code&gt;/search&lt;/code&gt;, you can use UUID in the &lt;code&gt;equals&lt;/code&gt; filter. If you request &lt;code&gt;id&lt;/code&gt; in &lt;code&gt;_source&lt;/code&gt;, the result will contain the same UUID in both &lt;code&gt;_id&lt;/code&gt; and &lt;code&gt;_source.id&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;-sS&lt;/span&gt; http://localhost:9308/search &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "table": "products_uuid",
    "query": {
      "equals": {
        "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
      }
    },
    "_source": ["id", "title", "sku", "price"]
  }'&lt;/span&gt;
&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;"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="s2"&gt;"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"&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;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;"_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;"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;"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"&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;"Wireless keyboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JSON-KB-001"&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;159&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;&lt;code&gt;_id&lt;/code&gt; is part of the search result metadata, while &lt;code&gt;_source.id&lt;/code&gt; is the document field. For a UUID table, they contain the same string.&lt;/p&gt;

&lt;p&gt;Now let's call the remaining endpoints for modifying data one by one. &lt;code&gt;UPDATE&lt;/code&gt; changes only the price:&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; http://localhost:9308/update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "table": "products_uuid",
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "doc": {"price": 149}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To replace the document completely, call &lt;code&gt;/replace&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;-sS&lt;/span&gt; http://localhost:9308/replace &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "table": "products_uuid",
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "doc": {
      "title": "Wireless keyboard, revised",
      "sku": "JSON-KB-001",
      "price": 139
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now delete the replaced document:&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; http://localhost:9308/delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "table": "products_uuid",
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;/delete&lt;/code&gt;, there is no document with this UUID left in the table. A &lt;code&gt;/search&lt;/code&gt; by the same ID will return &lt;code&gt;total: 0&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;-sS&lt;/span&gt; http://localhost:9308/search &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "table": "products_uuid",
    "query": {
      "equals": {
        "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
      }
    }
  }'&lt;/span&gt;
&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;"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;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;"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="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;Examples of requests for deleting by ID and by condition are collected in the documentation section on &lt;a href="https://manual.manticoresearch.com/dev/Data_creation_and_modification/Deleting_documents" rel="noopener noreferrer"&gt;deleting documents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to generate UUID
&lt;/h2&gt;

&lt;p&gt;If the UUID is already issued by the primary database, just pass it to Manticore as &lt;code&gt;id&lt;/code&gt;. If the request is repeated, the UUID will stay the same. &lt;code&gt;INSERT&lt;/code&gt; will report a duplicate, and &lt;code&gt;REPLACE&lt;/code&gt; will write a new version of the document under the same ID. The same rule applies to &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;replace&lt;/code&gt; in &lt;code&gt;/bulk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Manticore can also generate the UUID itself: just do not pass &lt;code&gt;id&lt;/code&gt;. But resending such a request will create another document, so for automatic retries it is better to set the UUID explicitly.&lt;/p&gt;

&lt;p&gt;For an explicit ID, any UUID version from v1 to v8 is suitable. For automatic generation, Manticore uses its own UUIDv8 structure, but it does not convert UUIDs received from the client into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch loading through &lt;code&gt;/bulk&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/bulk&lt;/code&gt; accepts NDJSON: each line contains a separate operation. The UUID is passed in the &lt;code&gt;id&lt;/code&gt; field, just like in the other JSON API requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /bulk
Content-Type: application/x-ndjson

{"insert":{"table":"products_uuid","id":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb","doc":{"title":"USB hub","sku":"BULK-HUB-001","price":49}}}
{"insert":{"table":"products_uuid","id":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbc","doc":{"title":"Laptop stand","sku":"BULK-STAND-001","price":39}}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the last data line, a trailing newline is required. With &lt;code&gt;curl&lt;/code&gt;, it is convenient to pass the body through &lt;code&gt;--data-binary&lt;/code&gt; so it does not strip newlines.&lt;/p&gt;

&lt;p&gt;Both operations belong to one table, so Manticore executes them in a single transaction. The shortened response shows how many documents were added and how the entire batch finished:&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;"items"&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;"bulk"&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;"created"&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="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;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;"current_line"&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;"skipped_lines"&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;"errors"&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="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;On an error, &lt;code&gt;current_line&lt;/code&gt; shows the line where processing stopped, and &lt;code&gt;skipped_lines&lt;/code&gt; shows the number of skipped lines. If an empty line or a table switch split the request into multiple transactions, Manticore will not roll back the ones that already completed.&lt;/p&gt;

&lt;p&gt;When reloading a document, it is important to choose the right operation. If the UUID already exists, &lt;code&gt;insert&lt;/code&gt; will fail with a duplicate error. &lt;code&gt;replace&lt;/code&gt; will write the document again, and if no such ID exists, it will add a new document.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; field in &lt;code&gt;/bulk&lt;/code&gt; can also be omitted, and Manticore will generate a UUID. But the &lt;code&gt;/bulk&lt;/code&gt; response contains only the overall transaction result, without separate results for each inserted row. If you need to keep the ID of each document, it is more convenient to generate the UUID before the batch request or add documents one by one through &lt;code&gt;/insert&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and common errors
&lt;/h2&gt;

&lt;p&gt;Manticore validates the UUID before adding the document. SQL, the JSON API, and &lt;code&gt;/bulk&lt;/code&gt; return errors differently, so do not bind your code to the exact wording of the message.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you sent&lt;/th&gt;
&lt;th&gt;What Manticore will do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;550e8400-e29b-41d4-a716-446655440000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add the document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add the document, store the UUID, and return it in lowercase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A string without hyphens or with an invalid character&lt;/td&gt;
&lt;td&gt;Return an error because the UUID format is invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;00000000-0000-0000-0000-000000000000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return an error: zero UUID cannot be used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A number, including &lt;code&gt;0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Return an error: &lt;code&gt;id&lt;/code&gt; must be a string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A UUID with a version outside the 1-8 range or an RFC-invalid &lt;code&gt;variant&lt;/code&gt; value&lt;/td&gt;
&lt;td&gt;Return a validation error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;insert&lt;/code&gt; in &lt;code&gt;/bulk&lt;/code&gt; with an already existing ID&lt;/td&gt;
&lt;td&gt;Return a duplicate error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The canonical form consists of 36 characters split into &lt;code&gt;8-4-4-4-12&lt;/code&gt; groups. UUID versions from 1 to 8 are valid; in the RFC &lt;code&gt;variant&lt;/code&gt; position, one of these characters must appear: &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;.&lt;/li&gt;
&lt;li&gt;Manticore validates only the ID format. Correct generation is the application's responsibility: for UUIDv4, randomness matters, and for UUIDv7, the time component and compliance with the rules matter. Manticore does not convert passed v4 and v7 values into v8.&lt;/li&gt;
&lt;li&gt;In a table with UUID IDs, &lt;code&gt;id = 0&lt;/code&gt; does not trigger automatic generation unlike numeric IDs: the request will fail. But if you do not pass &lt;code&gt;id&lt;/code&gt;, Manticore will generate its own UUIDv8 structure and return it in the response. Keep in mind that it can be used as a document ID, but not as, for example, an access token.&lt;/li&gt;
&lt;li&gt;You can validate the UUID with a standard library on input, but keep in mind that Manticore still performs its own validation.&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;/bulk&lt;/code&gt; response, check &lt;code&gt;errors&lt;/code&gt;, &lt;code&gt;current_line&lt;/code&gt;, and &lt;code&gt;skipped_lines&lt;/code&gt;: they show where processing stopped and which part of the batch could not be committed. In SQL batch requests, the entire request is rolled back if an error occurs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Columnar RT and replication
&lt;/h2&gt;

&lt;p&gt;UUID can also be used as the document ID in RT tables with columnar storage. Only the engine declaration changes:&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_columnar&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="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'columnar'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For regular and columnar RT tables, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;REPLACE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;, exact-match searches, and &lt;code&gt;IN&lt;/code&gt; conditions are written the same way. The client code does not depend on the table engine. But &lt;code&gt;UPDATE&lt;/code&gt; does not change columnar attributes, so, for example, the price in such a table can only be updated together with the whole document through &lt;code&gt;REPLACE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Replication also supports UUID. Suppose the &lt;code&gt;catalog&lt;/code&gt; cluster has already been created, the nodes have joined, and the local &lt;code&gt;products_uuid&lt;/code&gt; table exists. Let's add it to 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;ALTER&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In SQL, a colon goes between the cluster name and the table name:&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="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;:&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-446655441000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Replicated keyboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'REPL-KB-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;169&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&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;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;products_uuid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'550e8400-e29b-41d4-a716-446655441000'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the JSON API, the table name is passed unchanged, while the cluster goes in a separate field. For example, the following operation will update a document previously added through SQL:&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; http://localhost:9308/update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{
    "cluster": "catalog",
    "table": "products_uuid",
    "id": "550e8400-e29b-41d4-a716-446655441000",
    "doc": {"price": 159}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After replication, the document keeps the same UUID on all nodes. You can run exact-match searches, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;REPLACE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; by it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to consider before rollout
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;uuid&lt;/code&gt; type can be assigned only to the &lt;code&gt;id&lt;/code&gt; field. It is supported in RT tables, including those with columnar storage and replication, but not in plain, percolate/PQ, or shard tables.&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;. This decision has to be made when creating the new schema.&lt;/li&gt;
&lt;li&gt;A UUID can be used in &lt;code&gt;=&lt;/code&gt; and &lt;code&gt;IN&lt;/code&gt; conditions. The &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; ranges, as well as arithmetic on &lt;code&gt;id&lt;/code&gt;, are not supported.&lt;/li&gt;
&lt;li&gt;UUIDv7 contains time, but filtering &lt;code&gt;id&lt;/code&gt; by range is still not allowed. For date-based selection, add an attribute such as &lt;code&gt;created_at timestamp&lt;/code&gt; and filter by it.&lt;/li&gt;
&lt;li&gt;The document ID cannot be changed with &lt;code&gt;UPDATE&lt;/code&gt;. To give an object a different UUID, you will need to create a document with a new ID and delete the old one separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The behavior of SQL sessions and the full list of limitations are described in the documentation: &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-6-6/" rel="noopener noreferrer"&gt;Manticore Search 28.6.6&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>search</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <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>
  </channel>
</rss>
