<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>媒资系统 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%AA%92%E8%B5%84%E7%B3%BB%E7%BB%9F/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sat, 28 Aug 2021 14:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E5%AA%92%E8%B5%84%E7%B3%BB%E7%BB%9F/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Elasticsearch 从原理到 IPTV 媒资搜索实战：基于 ftmao-meta 项目的完整复盘</title>
      <link>https://ftmao.com/blog/posts/elasticsearch-in-action-iptv/</link>
      <pubDate>Sat, 28 Aug 2021 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/elasticsearch-in-action-iptv/</guid>
      <description>&lt;h1 id=&#34;elasticsearch-从原理到-iptv-媒资搜索实战基于-ftmao-meta-项目的完整复盘&#34;&gt;Elasticsearch 从原理到 IPTV 媒资搜索实战：基于 ftmao-meta 项目的完整复盘&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;以我们线上运行的 IPTV 媒资元数据服务 &lt;strong&gt;ftmao-meta&lt;/strong&gt; 为样本，讲清楚一个真实的 ES（实际为 AWS 托管 OpenSearch）应用是怎么设计索引、同步数据、支撑搜索和下游订阅的。所有配置、代码、索引设计均来自项目本身，踩过的坑也一并奉上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一项目背景这是一个什么系统&#34;&gt;一、项目背景：这是一个什么系统&lt;/h2&gt;
&lt;p&gt;ftmao-meta 是一个面向多地区运营的 IPTV/OTT 视频内容平台的&lt;strong&gt;媒资元数据微服务&lt;/strong&gt;，Spring Boot 3.3 + Java 21，单模块 Gradle 工程，包结构 &lt;code&gt;gtt.ftmao.meta&lt;/code&gt;，分 &lt;code&gt;api / app / bean / core&lt;/code&gt; 四层。它管理的数据包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;影视元数据&lt;/strong&gt;：电影、剧集、季、集、短视频的多语言标题、简介、海报、演职员、分级、票房；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;演职人员&lt;/strong&gt;：多语言姓名、热度值（pgcHot / ugcHot / imdbHot）、代表作；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物料&lt;/strong&gt;：剧照海报（artwork）、预告片（trailer）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C 端搜索&lt;/strong&gt;：全文检索 + 输入自动补全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些数据量巨大（千万级文档）、多语言（英语/西语/葡语，服务拉美市场）、多租户（多家运营商公司共用），而且需要被下游多个服务&lt;strong&gt;增量订阅&lt;/strong&gt;。MySQL 是事实源（Source of Truth），ES 承担所有读路径的检索。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术选型上的关键事实&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后端是 &lt;strong&gt;AWS 托管 OpenSearch&lt;/strong&gt;，客户端用官方 &lt;code&gt;opensearch-java 2.8.1&lt;/code&gt;（&lt;code&gt;org.opensearch.client.opensearch.OpenSearchClient&lt;/code&gt;），&lt;strong&gt;没有用 spring-data-elasticsearch，也没有 RestHighLevelClient&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;全文检索与自动补全能力封装在公司内部库 &lt;code&gt;searchengine-api/core/sync&lt;/code&gt; 里，业务代码只调用 &lt;code&gt;SearchApi&lt;/code&gt; / &lt;code&gt;SearchSync&lt;/code&gt; 门面；&lt;/li&gt;
&lt;li&gt;数据同步&lt;strong&gt;不走 MQ&lt;/strong&gt;（Kafka 依赖在启动类里被显式排除），而是&amp;quot;MySQL 任务表 + 定时轮询 + 分布式锁&amp;quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 上游[&amp;#34;上游数据来源&amp;#34;]
        BOSS[&amp;#34;运营后台 / 内容录入&amp;#34;]
        PP[&amp;#34;PP 开放平台&amp;lt;br/&amp;gt;（REST 拉取外部媒资）&amp;#34;]
    end

    subgraph META[&amp;#34;ftmao-meta 服务&amp;#34;]
        API[&amp;#34;REST API 层&amp;#34;]
        SVC[&amp;#34;业务 Service&amp;lt;br/&amp;gt;MetaService / ArtworkService ...&amp;#34;]
        JOB[&amp;#34;定时 Job&amp;lt;br/&amp;gt;PpMetaSyncJob 每 15s&amp;#34;]
        LOCK[&amp;#34;分布式锁&amp;lt;br/&amp;gt;AsyncJobExecutorManager&amp;#34;]
    end

    subgraph STORE[&amp;#34;存储层&amp;#34;]
        MYSQL[(&amp;#34;MySQL&amp;lt;br/&amp;gt;事实源 + 同步任务表&amp;#34;)]
        subgraph OS[&amp;#34;AWS OpenSearch&amp;#34;]
            DETAIL[&amp;#34;meta_detail_index&amp;lt;br/&amp;gt;媒资详情（重索引）&amp;#34;]
            PERSON[&amp;#34;meta_person_index&amp;lt;br/&amp;gt;人物详情&amp;#34;]
            ART[&amp;#34;meta_artwork_index&amp;lt;br/&amp;gt;剧照物料&amp;#34;]
            TRAILER[&amp;#34;meta_trailer_index&amp;lt;br/&amp;gt;预告片&amp;#34;]
            SEARCH[&amp;#34;search_engine_index&amp;lt;br/&amp;gt;C端搜索（轻索引）&amp;#34;]
        end
    end

    subgraph DOWN[&amp;#34;下游消费方&amp;#34;]
        PORTAL[&amp;#34;门户/推荐服务&amp;#34;]
        SUB[&amp;#34;增量订阅方&amp;lt;br/&amp;gt;（时间水位拉取变更）&amp;#34;]
    end

    BOSS --&amp;gt; SVC
    PP --&amp;gt; JOB
    SVC --&amp;gt;|&amp;#34;事务内：先写库，再直写 ES&amp;#34;| MYSQL
    SVC --&amp;gt; DETAIL
    SVC --&amp;gt; SEARCH
    JOB --&amp;gt; LOCK --&amp;gt; MYSQL
    JOB --&amp;gt;|&amp;#34;批量灌入&amp;#34;| SEARCH
    API --&amp;gt;|&amp;#34;全文搜索/补全&amp;#34;| SEARCH
    API --&amp;gt;|&amp;#34;mget 回查详情&amp;#34;| DETAIL
    API --&amp;gt; PERSON &amp;amp; ART &amp;amp; TRAILER
    DETAIL --&amp;gt;|&amp;#34;lastModifiedIndicator 水位&amp;#34;| SUB
    SEARCH --&amp;gt; PORTAL&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;二先补原理理解后面设计决策的三个支点&#34;&gt;二、先补原理：理解后面设计决策的三个支点&lt;/h2&gt;
&lt;p&gt;在进入项目细节前，用三句话回顾《Elasticsearch in Action》里最重要的原理，后面每个设计决策都会用到它们。&lt;/p&gt;
&lt;h3 id=&#34;21-倒排索引与分词&#34;&gt;2.1 倒排索引与分词&lt;/h3&gt;
&lt;p&gt;ES 搜索快的根本原因是&lt;strong&gt;倒排索引&lt;/strong&gt;：term → 文档列表。而文本进入倒排索引前要经过分析链：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;原始文本&amp;lt;br/&amp;gt;O Auto da Compadecida&amp;#34;] --&amp;gt; B[&amp;#34;Character Filter&amp;lt;br/&amp;gt;变音符号映射 á→a&amp;#34;]
    B --&amp;gt; C[&amp;#34;Tokenizer&amp;lt;br/&amp;gt;standard 分词&amp;#34;]
    C --&amp;gt; D[&amp;#34;Token Filter&amp;lt;br/&amp;gt;lowercase + stopword + stemmer&amp;#34;]
    D --&amp;gt; E[&amp;#34;词项 Term&amp;lt;br/&amp;gt;auto / compadecida&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一个字段建索引时用哪个 analyzer、搜索时用哪个 analyzer，直接决定了&amp;quot;搜不搜得到&amp;quot;。&lt;strong&gt;这就是为什么 mapping 必须在灌数据之前设计好&lt;/strong&gt;——改 analyzer 意味着重建索引。&lt;/p&gt;
&lt;h3 id=&#34;22-近实时nrt与-refresh&#34;&gt;2.2 近实时（NRT）与 refresh&lt;/h3&gt;
&lt;p&gt;文档写入后先进内存缓冲区，默认 1 秒后 refresh 生成 segment 才可被搜索。批量灌数据时调大 &lt;code&gt;refresh_interval&lt;/code&gt;、或显式控制 &lt;code&gt;refresh&lt;/code&gt; 时机，是写入性能优化的第一手段——后面会看到项目里批量写入怎么用它。&lt;/p&gt;
&lt;h3 id=&#34;23-query-与-filter-的区别&#34;&gt;2.3 Query 与 Filter 的区别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Query&lt;/strong&gt;：算相关性得分 &lt;code&gt;_score&lt;/code&gt;，用于全文匹配排序；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filter&lt;/strong&gt;：只判断 yes/no，&lt;strong&gt;不算分、结果可被缓存&lt;/strong&gt;，用于精确过滤。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;项目里几乎所有查询都是&amp;quot;filter 套 query&amp;quot;的 bool 结构：关键词进 &lt;code&gt;must&lt;/code&gt;，租户、类型、软删除标记进 &lt;code&gt;filter&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三索引设计一个搜索索引--四个详情索引&#34;&gt;三、索引设计：一个搜索索引 + 四个详情索引&lt;/h2&gt;
&lt;h3 id=&#34;31-索引清单&#34;&gt;3.1 索引清单&lt;/h3&gt;
&lt;p&gt;项目中&lt;strong&gt;没有任何 &lt;code&gt;@Document&lt;/code&gt; 注解或 mapping JSON 文件&lt;/strong&gt;，索引 DDL 采用类似 Liquibase 的 changelog 形式管理（&lt;code&gt;changelog/elasticsearch/*.yaml&lt;/code&gt;，HTTP method + path + body，由外部部署工具执行），别名字段统一管理：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;代码中的别名&lt;/th&gt;
					&lt;th&gt;实体索引&lt;/th&gt;
					&lt;th&gt;分片&lt;/th&gt;
					&lt;th&gt;存什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;meta_detail_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-detail-1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;媒资详情全集&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;meta_person_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-person-detail&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;演职人员详情&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;meta_artwork_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-media-artwork&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;剧照物料&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;meta_trailer_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-media-trailer&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;预告片&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;search_engine_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-search-engine&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;1 分片&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;C 端搜索/补全&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;别名与文档类的映射集中在一个枚举里（&lt;code&gt;OpenSearchIndexEnum&lt;/code&gt;），业务代码只认别名——这是&amp;quot;换索引不改代码&amp;quot;的基本功，比如 &lt;code&gt;ftmao-meta-detail-1&lt;/code&gt; 后面出现 &lt;code&gt;-2&lt;/code&gt;，只需要 reindex 后切别名。&lt;/p&gt;
&lt;h3 id=&#34;32-详情索引重存储轻检索&#34;&gt;3.2 详情索引：重存储、轻检索&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;meta_detail_index&lt;/code&gt; 的 mapping 有个鲜明特征：&lt;strong&gt;几乎所有字段都是 keyword，标题、剧情简介都是 keyword + nested，不参与分词检索&lt;/strong&gt;，大量 nested 对象（displayTitles / plots / posters / casts）配 &lt;code&gt;dynamic: false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为什么？因为详情索引的职责是**&amp;ldquo;按 ID/结构化条件精确取数&amp;rdquo;**，不是全文搜索。keyword 不做分词，聚合、terms 过滤、精确匹配都快；&lt;code&gt;dynamic: false&lt;/code&gt; 防止上游数据乱塞字段把 mapping 撑爆（mapping explosion 是生产事故常客）。&lt;/p&gt;
&lt;h3 id=&#34;33-搜索索引整个项目最精彩的部分&#34;&gt;3.3 搜索索引：整个项目最精彩的部分&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;search_engine_index&lt;/code&gt; 只有 1 个分片（轻量、追求查询 RT），但它的 analysis 配置是整个项目技术含量最高的地方。以主搜索字段 &lt;code&gt;mainTitle&lt;/code&gt; 为例：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 摘自 changelog（简化）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;mainTitle&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;store&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;term_vector&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;with_positions_offsets  &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 为 fast-vector-highlighter 高亮做准备&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;analyzer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;en_es_pt                    &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 英/西/葡三语混合分词器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;fields&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;cut_word&lt;/span&gt;:    { &lt;span style=&#34;color:#f92672&#34;&gt;analyzer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;en_es_pt }             &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 正常分词匹配&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;all_letters&lt;/span&gt;: { &lt;span style=&#34;color:#f92672&#34;&gt;analyzer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;N_gram(4-15) }         &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 长 ngram：前缀/模糊匹配&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ae81ff&#34;&gt;word_letters:{ analyzer: N_gram(2-10) }         &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 短 ngram：更激进的模糊匹配&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;origin&lt;/span&gt;:      { &lt;span style=&#34;color:#f92672&#34;&gt;type: keyword, normalizer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;lowercase } &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 精确匹配/排序&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;配套自定义组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多语分析器 &lt;code&gt;en_es_pt&lt;/code&gt;&lt;/strong&gt;：standard tokenizer + 英语/西语/葡语三套 stopword + stemmer；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变音符号 char_filter&lt;/strong&gt;：把 &lt;code&gt;áéíóúàèç&lt;/code&gt; 等西葡变音符号映射为 ASCII——拉美用户搜 &lt;code&gt;compadecida&lt;/code&gt; 也能命中 &lt;code&gt;Compadecida&lt;/code&gt;，&lt;strong&gt;对多语言市场这是体验生死线&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ngram tokenizer&lt;/strong&gt; 一批（1–10、4–15 等不同窗口），&lt;code&gt;max_ngram_diff=14&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;completion suggester&lt;/strong&gt;：&lt;code&gt;suggest&lt;/code&gt; 字段配 &lt;code&gt;suggester_trigram&lt;/code&gt; 分析器，带 3 个 &lt;strong&gt;category context&lt;/strong&gt;（&lt;code&gt;markCate&lt;/code&gt; / &lt;code&gt;tenantCate&lt;/code&gt; / &lt;code&gt;comboCate&lt;/code&gt;）——自动补全可以按内容类型和租户隔离，A 运营商的用户不会看到 B 运营商专有的片名提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;设计启示&lt;/strong&gt;：一个&amp;quot;能搜&amp;quot;的字段，实际需要四层子字段分工——正常分词管召回、长 ngram 管前缀联想、短 ngram 管纠错、keyword 管精确和排序。这也正是《Elasticsearch in Action》里&amp;quot;多字段映射&amp;quot;思想的生产级演绎。&lt;/p&gt;
&lt;h3 id=&#34;34-双索引架构为什么搜索和详情要分开&#34;&gt;3.4 双索引架构：为什么搜索和详情要分开&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    subgraph 写入[&amp;#34;写入侧&amp;#34;]
        D[&amp;#34;MetaDetail 完整文档&amp;lt;br/&amp;gt;（几十 KB，含 nested 海报/演员/多语言）&amp;#34;] --&amp;gt; DETAIL[&amp;#34;详情索引&amp;lt;br/&amp;gt;6分片 重存储&amp;#34;]
        D --&amp;gt;|&amp;#34;裁剪：docId + mainTitle +&amp;lt;br/&amp;gt;suggest + 过滤字段 + 租户&amp;#34;| SEARCH[&amp;#34;搜索索引&amp;lt;br/&amp;gt;1分片 轻量&amp;#34;]
    end
    subgraph 查询[&amp;#34;查询侧&amp;#34;]
        U[&amp;#34;用户搜索 &amp;#39;auto&amp;#39;&amp;#34;] --&amp;gt;|&amp;#34;① matchSearch 命中&amp;lt;br/&amp;gt;返回 docId 列表&amp;#34;| SEARCH
        SEARCH --&amp;gt;|&amp;#34;② 按 docId mget&amp;#34;| DETAIL
        DETAIL --&amp;gt;|&amp;#34;组装完整详情返回&amp;#34;| U
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;搜索索引里&lt;strong&gt;不存详情&lt;/strong&gt;，只有：docId、mainTitle（及 ngram 子字段）、suggest、40+ 个 keyword 过滤字段（&lt;code&gt;ext-field-to-index-map&lt;/code&gt; 里配置，如媒资类型、地区、年份、分级）、租户字段。好处有三个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;搜索索引小&lt;/strong&gt;，1 个分片 + 内存友好，C 端 QPS 扛得住；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;详情变更不用动搜索索引&lt;/strong&gt;——海报换了只 update 详情索引；搜索索引只在标题/过滤字段变化时更新；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两类索引可以独立扩缩容、独立重建&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代价也很明显：多一次 mget 回查、数据一致性要维护两份。这个项目用下一节的同步机制来兜底。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四数据同步mysql--es-的三条路径&#34;&gt;四、数据同步：MySQL → ES 的三条路径&lt;/h2&gt;
&lt;p&gt;ES 是&amp;quot;读模型&amp;quot;，MySQL 才是事实源。项目没用 Canal/MQ，而是自己实现了一套&lt;strong&gt;事务内直写 + 任务表轮询&lt;/strong&gt;的混合方案：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;业务写请求&amp;lt;br/&amp;gt;MetaService.save()&amp;#34;] --&amp;gt; B{&amp;#34;@Transactional&amp;#34;}
    B --&amp;gt; C[&amp;#34;① 写 MySQL 业务表&amp;#34;]
    B --&amp;gt; D[&amp;#34;② 写 MySQL 同步任务表&amp;lt;br/&amp;gt;MixedMetaSyncTask&amp;#34;]
    B --&amp;gt; E[&amp;#34;③ 同事务内直写详情索引&amp;lt;br/&amp;gt;bulkOperationByBatch(refresh=True)&amp;#34;]
    B --&amp;gt; F[&amp;#34;④ saveMetaToSearchEngine&amp;lt;br/&amp;gt;同步搜索索引&amp;#34;]
    E --&amp;gt; G{&amp;#34;ES 写失败?&amp;#34;}
    G --&amp;gt;|&amp;#34;是，事务回滚或留下任务&amp;#34;| H[&amp;#34;任务表残留记录&amp;#34;]
    D --&amp;gt; I[&amp;#34;PpMetaSyncJob&amp;lt;br/&amp;gt;@Scheduled 每 15s&amp;#34;]
    H --&amp;gt; I
    I --&amp;gt; J[&amp;#34;分布式锁&amp;lt;br/&amp;gt;executeWithLock&amp;#34;]
    J --&amp;gt; K[&amp;#34;MetaTaskProcessService&amp;lt;br/&amp;gt;分批 Lists.partition(batchSize)&amp;#34;]
    K --&amp;gt;|&amp;#34;searchSync.save(batch)&amp;#34;| L[&amp;#34;搜索索引&amp;#34;]
    M[&amp;#34;失败重试 Job&amp;lt;br/&amp;gt;每小时&amp;#34;] --&amp;gt; J&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三条路径各有分工：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同事务直写&lt;/strong&gt;（主路径）：保证&amp;quot;后台保存完，前台立刻能搜到&amp;quot;。写入时统一刷新 &lt;code&gt;updateTime&lt;/code&gt; 和 &lt;code&gt;lastModifiedIndicator&lt;/code&gt;（格式：&lt;code&gt;时间戳_docId&lt;/code&gt; 的版本游标，下一节详述）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务表 + 定时轮询&lt;/strong&gt;（兜底路径）：15 秒一轮，分布式锁防多实例并发，分批灌入；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败重试 Job&lt;/strong&gt;（保险）：每小时扫描失败任务重放。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;为什么不纯用 MQ？&lt;/strong&gt; 这个系统的写入量级不大（媒资是低频高价值数据，不是日志），事务内直写的一致性收益远大于引入 MQ 的复杂度。这是&amp;quot;按场景选方案&amp;quot;而不是&amp;quot;按潮流选方案&amp;quot;的好例子。&lt;/p&gt;
&lt;h3 id=&#34;41-批量写入的性能细节&#34;&gt;4.1 批量写入的性能细节&lt;/h3&gt;
&lt;p&gt;工具类 &lt;code&gt;OpenSearchUtil.bulkUpsertDocuments&lt;/code&gt; 里有两个值得抄的实践：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 按配置的 bulkSize（默认 200）分批，避免单个 bulk 过大压垮集群&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 每批显式 refresh(WaitFor)：写完后“等 refresh 完成再返回”，&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 保证同步接口的调用方&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;“&lt;/span&gt;写完立即可见&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;”，&lt;/span&gt;同时比 refresh&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;（&lt;/span&gt;每文档刷新&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;）&lt;/span&gt;省资源&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;对照第 2.2 节的 NRT 原理：&lt;code&gt;refresh=WaitFor&lt;/code&gt; 是吞吐和可见性之间的甜点——批量场景不要无脑 &lt;code&gt;refresh=true&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五查询实战从业务代码反推-es-用法&#34;&gt;五、查询实战：从业务代码反推 ES 用法&lt;/h2&gt;
&lt;h3 id=&#34;51-一切查询的起点软删除过滤&#34;&gt;5.1 一切查询的起点：软删除过滤&lt;/h3&gt;
&lt;p&gt;项目里文档不物理删除，而是软删除——所有读路径都要叠一层 &lt;code&gt;isDeleted=0&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// OpenSearchUtil.getUndeletedQuery：所有查询都要过这层包装&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Query undeletedQuery &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;getUndeletedQuery&lt;/span&gt;(boolQueryBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;_toQuery&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 底层就是 bool.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;(terms(isDeleted, &lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;0&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;))&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;可能再叠加 status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;1&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;（&lt;/span&gt;已发布&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;）&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;业务侧的典型用法（&lt;code&gt;ArtworkService&lt;/code&gt;，按媒资 ID 查剧照）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Query metaIdQuery &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Query.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(q &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; q.&lt;span style=&#34;color:#a6e22e&#34;&gt;terms&lt;/span&gt;(t &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; t.&lt;span style=&#34;color:#a6e22e&#34;&gt;field&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;metaId&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;terms&lt;/span&gt;(tv &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; tv.&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;(Stream.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(request.&lt;span style=&#34;color:#a6e22e&#34;&gt;getMetaId&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt;(FieldValue::of).&lt;span style=&#34;color:#a6e22e&#34;&gt;toList&lt;/span&gt;()))));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;boolQueryBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;(metaIdQuery);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Query undeletedQuery &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;getUndeletedQuery&lt;/span&gt;(boolQueryBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;_toQuery&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SortOptions sortOption &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; SortOptions.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(s &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; s.&lt;span style=&#34;color:#a6e22e&#34;&gt;field&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    f &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; f.&lt;span style=&#34;color:#a6e22e&#34;&gt;field&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pos&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;(SortOrder.&lt;span style=&#34;color:#a6e22e&#34;&gt;Asc&lt;/span&gt;)));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;MediaArtwork&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; artworks &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;searchDoc&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    OpenSearchIndexEnum.&lt;span style=&#34;color:#a6e22e&#34;&gt;MEDIA_ARTWORK&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getIndexName&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    undeletedQuery, MediaArtwork.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    request.&lt;span style=&#34;color:#a6e22e&#34;&gt;getPageId&lt;/span&gt;(), request.&lt;span style=&#34;color:#a6e22e&#34;&gt;getPageSize&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Collections.&lt;span style=&#34;color:#a6e22e&#34;&gt;singletonList&lt;/span&gt;(sortOption), totalCount);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这正是第 2.3 节&amp;quot;filter 不算分、可缓存&amp;quot;原理的落地：&lt;strong&gt;结构化条件全部进 filter&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;52-软删除的实现updatebyquery--painless-脚本&#34;&gt;5.2 软删除的实现：updateByQuery + Painless 脚本&lt;/h3&gt;
&lt;p&gt;删除不是 &lt;code&gt;delete&lt;/code&gt;，而是 &lt;code&gt;updateByQuery&lt;/code&gt; 打标记：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;String script &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; ctx._source.isDeleted = 1;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ctx._source.updateTime = params.currentTime;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ctx._source.lastModifiedIndicator = params.indicatorPrefix + &amp;#39;_&amp;#39; + ctx._id&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;UpdateByQueryRequest request &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; UpdateByQueryRequest.&lt;span style=&#34;color:#a6e22e&#34;&gt;Builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;index&lt;/span&gt;(indexName).&lt;span style=&#34;color:#a6e22e&#34;&gt;query&lt;/span&gt;(query)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;script&lt;/span&gt;(s &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; s.&lt;span style=&#34;color:#a6e22e&#34;&gt;inline&lt;/span&gt;(i &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; i.&lt;span style=&#34;color:#a6e22e&#34;&gt;source&lt;/span&gt;(finalScript)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;params&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currentTime&amp;#34;&lt;/span&gt;, JsonData.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(currentTime))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;params&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;indicatorPrefix&amp;#34;&lt;/span&gt;, JsonData.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(String.&lt;span style=&#34;color:#a6e22e&#34;&gt;valueOf&lt;/span&gt;(modifyTime)))))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意脚本里同时更新了 &lt;code&gt;lastModifiedIndicator&lt;/code&gt;——&lt;strong&gt;软删除也是一次&amp;quot;变更&amp;quot;，必须让下游订阅方感知到&lt;/strong&gt;（下游才能同步删）。这个细节是订阅体系能闭环的关键。&lt;/p&gt;
&lt;h3 id=&#34;53-两阶段全文搜索&#34;&gt;5.3 两阶段全文搜索&lt;/h3&gt;
&lt;p&gt;C 端搜索入口 &lt;code&gt;SearchService.searchMedia&lt;/code&gt; 的分流逻辑：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (StringUtils.&lt;span style=&#34;color:#a6e22e&#34;&gt;isNotEmpty&lt;/span&gt;(fuzzyMatchingParams.&lt;span style=&#34;color:#a6e22e&#34;&gt;getKeyword&lt;/span&gt;())) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 有关键词：内部库全文检索 → 返回 docId + mark（类型）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resp &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; searchApi.&lt;span style=&#34;color:#a6e22e&#34;&gt;matchSearch&lt;/span&gt;(keyword, marks, pageId, pageSize, tenantIds, params);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 无关键词：纯结构化筛选（找片页、分类浏览）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resp &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; searchApi.&lt;span style=&#34;color:#a6e22e&#34;&gt;find&lt;/span&gt;(marks, pageId, pageSize, tenantIds, params);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 按 mark 分流 person / meta，回查详情索引取完整数据&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;batchGetDocuments&lt;/span&gt;(PERSON_DETAIL.&lt;span style=&#34;color:#a6e22e&#34;&gt;getIndexName&lt;/span&gt;(), personDocIds, PersonDetail.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;batchGetDocuments&lt;/span&gt;(META_DETAIL.&lt;span style=&#34;color:#a6e22e&#34;&gt;getIndexName&lt;/span&gt;(), metaDocIds, MetaDetail.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;查询条件从哪来？API 层收的是 &lt;strong&gt;SQL 风格的 where 模板&lt;/strong&gt;（&lt;code&gt;MetaQuery.whereTemplate&lt;/code&gt; + &lt;code&gt;parameters&lt;/code&gt;），&lt;code&gt;WhereClauseUtil&lt;/code&gt; 用正则把 &lt;code&gt;in (:x)&lt;/code&gt;、&lt;code&gt;between and&lt;/code&gt;、&lt;code&gt;JSON_CONTAINS_ALL/ANY&lt;/code&gt; 解析成 ES 的 Condition（IN / GREATER_THAN 等），字段名经 &lt;code&gt;FieldNameMappingEnum&lt;/code&gt; 做外部名→索引字段名映射。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这个设计的好处&lt;/strong&gt;：对前端/上游服务来说，查询语法是稳定的数据库风格，不暴露 ES DSL；ES mapping 改名、换引擎，外部无感。&lt;/p&gt;
&lt;h3 id=&#34;54-自动补全completion-suggester--租户隔离&#34;&gt;5.4 自动补全：completion suggester + 租户隔离&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// SearchService.suggest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Prompt&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; prompts &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; searchApi.&lt;span style=&#34;color:#a6e22e&#34;&gt;getPromptList&lt;/span&gt;(keyword, marks, &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;, companyId, size, uuid);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 底层 = 搜索索引 suggest 字段的 completion suggester，&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// context 里带 tenantCate=companyId → 补全结果按运营商隔离&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 拿到 docId 后再回查详情索引组装标题和海报&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;举例如&amp;quot;用户输入 &lt;code&gt;int&lt;/code&gt;&amp;quot;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant U as 用户(运营商A)
    participant S as SearchService
    participant SE as 搜索索引 suggester
    participant D as 详情索引
    U-&amp;gt;&amp;gt;S: suggest(&amp;#34;int&amp;#34;, companyId=A)
    S-&amp;gt;&amp;gt;SE: completion suggest&amp;lt;br/&amp;gt;context: tenantCate=A
    SE--&amp;gt;&amp;gt;S: [&amp;#34;Interestelar&amp;#34;, &amp;#34;Intocáveis&amp;#34;]&amp;lt;br/&amp;gt;+ docId 列表
    S-&amp;gt;&amp;gt;D: mget(docIds)
    D--&amp;gt;&amp;gt;S: 标题 + 海报 URL
    S--&amp;gt;&amp;gt;U: 补全列表（带封面）&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;55-一个常被忽略的性能点只取-id-就别加载-_source&#34;&gt;5.5 一个常被忽略的性能点：只取 ID 就别加载 _source&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// fastGetDocumentById：判断文档存在/取 ID 时&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// storedFields(&amp;#34;_id&amp;#34;) + size(1) + trackTotalHits(false)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 注释原话&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;：&lt;/span&gt;类似 MySQL 的 LIMIT 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trackTotalHits(false)&lt;/code&gt;：不精确统计总数（默认只算到 10000），省掉一次全量计数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storedFields(&amp;quot;_id&amp;quot;)&lt;/code&gt;：不加载 &lt;code&gt;_source&lt;/code&gt;，网络传输量从几十 KB 降到几十 B。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;列表页&amp;quot;先查 ID 再回查&amp;quot;的场景，这两招能把响应时间砍掉一个量级。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六增量订阅es-作为变更数据源&#34;&gt;六、增量订阅：ES 作为&amp;quot;变更数据源&amp;quot;&lt;/h2&gt;
&lt;p&gt;下游服务（推荐、缓存、其他中台）需要感知媒资变更。项目没有用 CDC 或 MQ 广播，而是让下游&lt;strong&gt;基于水位游标主动拉取 ES&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant SUB as 下游订阅方
    participant Q as SubqueryService
    participant ES as 详情索引

    Note over SUB: 记录上次水位 W0&amp;lt;br/&amp;gt;（lastModifiedIndicator 字符串）
    SUB-&amp;gt;&amp;gt;Q: querySubQuery(from=W0, size=500)
    Q-&amp;gt;&amp;gt;ES: range(lastModifiedIndicator: gt W0, lt 当前时间-2s)&amp;lt;br/&amp;gt;+ 按 lastModifiedIndicator 升序
    ES--&amp;gt;&amp;gt;Q: 本批变更文档（含软删除的）
    Q--&amp;gt;&amp;gt;SUB: 文档 + 新水位 W1
    SUB-&amp;gt;&amp;gt;SUB: 落库/更新缓存，记录 W1
    Note over Q,ES: 对账：subQueryCount 用 count + sort desc + size(1)&amp;lt;br/&amp;gt;取区间最大水位，生产方/消费方核对数量&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个工程细节值得细说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;水位是字符串而非时间戳&lt;/strong&gt;：&lt;code&gt;lastModifiedIndicator = 时间戳 + &amp;quot;_&amp;quot; + docId&lt;/code&gt;。纯时间戳会在同一毫秒内有多个文档时漏数；拼上 docId 后字典序唯一，range &lt;code&gt;gt&lt;/code&gt; 翻页不会漏、不会重。这是项目&lt;strong&gt;用手写游标替代 &lt;code&gt;search_after&lt;/code&gt;&lt;/strong&gt; 的原因——行为完全可控，且不依赖 ES 的 PIT（point in time）上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lt 当前时间 - 2s&lt;/code&gt; 的回调偏移&lt;/strong&gt;：规避&amp;quot;刚写入还没 refresh 可见&amp;quot;的窗口（第 2.2 节 NRT 原理的直接应用），宁可晚 2 秒，不可漏数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软删除文档也带着新水位出现&lt;/strong&gt;，下游据此删除本地副本——和第 5.2 节的脚本闭环。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七多租户设计docid-里藏着租户&#34;&gt;七、多租户设计：docId 里藏着租户&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// OpenSearchUtil.getDocId：文档 ID = companyId + &amp;#34;#&amp;#34; + metaId&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 例&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;：&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1001#mv_8848123&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;、&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;public#mv_8848123&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;（&lt;/span&gt;公共库&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;）&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;同一部影片，A 运营商和 B 运营商是&lt;strong&gt;两份文档&lt;/strong&gt;（各自的标题、定价、上架状态可能不同），天然物理隔离；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sourceType&lt;/code&gt; 字段区分 PRIVATE（私有）/ PUBLIC（公共）/ MIXED（混合），公共内容用 &lt;code&gt;public#&lt;/code&gt; 前缀，查询时按租户策略合并；&lt;/li&gt;
&lt;li&gt;所有查询 filter 里都带 &lt;code&gt;companyId&lt;/code&gt; / &lt;code&gt;tenant_id&lt;/code&gt;，补全用 context 隔离（第 5.4 节）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;经验&lt;/strong&gt;：多租户搜索系统，把租户编进 docId 比单纯加过滤字段更稳——docId 冲突本身就是一道防串租户的保险。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八回到原理这个项目验证了哪些书里的论断&#34;&gt;八、回到原理：这个项目验证了哪些书里的论断&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;书中原理&lt;/th&gt;
					&lt;th&gt;项目里的体现&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Mapping 先行，改 analyzer 要重建索引&lt;/td&gt;
					&lt;td&gt;索引 DDL 用 changelog 版本化管理，上线流程化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Filter 不算分、可缓存&lt;/td&gt;
					&lt;td&gt;租户/类型/软删除/状态全部进 filter，只有关键词算分&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多字段映射解决&amp;quot;一个字段多种搜法&amp;quot;&lt;/td&gt;
					&lt;td&gt;mainTitle 四层子字段：分词/长ngram/短ngram/keyword&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;NRT 的 refresh 是性能旋钮&lt;/td&gt;
					&lt;td&gt;批量写入用 &lt;code&gt;refresh=WaitFor&lt;/code&gt;；订阅端留 2s 偏移&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;深度翻页要避免 from+size 深分页&lt;/td&gt;
					&lt;td&gt;手写 &lt;code&gt;lastModifiedIndicator&lt;/code&gt; 游标替代 search_after&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;聚合昂贵，慎用&lt;/td&gt;
					&lt;td&gt;项目几乎零聚合；取最大值用 sort+size(1) 替代 max agg&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;分片数创建时定死，规划要前置&lt;/td&gt;
					&lt;td&gt;详情索引 6 分片按数据增长预估；搜索索引刻意 1 分片&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;如果把这套架构压缩成一张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    W[&amp;#34;写路径&amp;lt;br/&amp;gt;MySQL 事务&amp;lt;br/&amp;gt;+ 任务表兜底&amp;#34;] --&amp;gt; ES
    subgraph ES[&amp;#34;OpenSearch&amp;#34;]
        S[&amp;#34;搜索索引&amp;lt;br/&amp;gt;小而快：ngram+suggester&amp;lt;br/&amp;gt;+租户 context&amp;#34;]
        D[&amp;#34;详情索引&amp;lt;br/&amp;gt;大而全：keyword+nested&amp;lt;br/&amp;gt;dynamic:false&amp;#34;]
    end
    ES --&amp;gt; R1[&amp;#34;读路径 ①&amp;lt;br/&amp;gt;搜索/补全 → docId&amp;#34;]
    R1 --&amp;gt; R2[&amp;#34;读路径 ②&amp;lt;br/&amp;gt;mget 详情组装&amp;#34;]
    ES --&amp;gt; R3[&amp;#34;订阅路径&amp;lt;br/&amp;gt;水位游标增量拉取&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三条核心经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;索引按&amp;quot;查询模式&amp;quot;而不是&amp;quot;数据表&amp;quot;来设计&lt;/strong&gt;：搜索索引和详情索引分开，一个为召回优化（ngram、suggester、多语分析器），一个为取数优化（keyword、nested、禁动态字段）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性靠机制不靠运气&lt;/strong&gt;：事务内直写是主路径，任务表 + 定时 Job + 失败重试是兜底，水位游标让下游最终一致——三层防护各管一段；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所有&amp;quot;黑科技&amp;quot;都是基本原理的应用&lt;/strong&gt;：refresh 偏移、filter 缓存、storedFields 取 ID、游标翻页……吃透《Elasticsearch in Action》那套原理，生产方案都是它的排列组合。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考资料：Radu Gheorghe, Matthew Lee Hinman, Roy Russo. 《Elasticsearch in Action》. Manning Publications, 2015；ftmao-meta 项目源码（OpenSearchConfig / OpenSearchUtil / SearchService / SubqueryService / changelog/elasticsearch）。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
