我们来详细拆解 MeiliSearch 的核心工作流程,从添加文档到执行检索的内部逻辑。理解这个过程对于高效使用和调优 MeiliSearch 至关重要。
核心概念回顾:
- 索引: 数据存储的核心单元。每个索引包含一组结构相似(但不必完全相同)的文档和为该索引定制的搜索规则(设置)。
- 文档: 存储在索引中的基本数据单元。通常是 JSON 对象。每个文档必须有一个唯一标识符(通常是
id字段)。 - 任务: MeiliSearch 中所有写操作(添加、更新、删除文档,更新设置)都是异步的,通过任务队列处理。客户端提交操作后会立即收到一个
taskId用于后续查询状态。 - 设置: 控制索引行为的规则,包括:
- 可搜索属性: 哪些字段的内容可以被搜索。
- 过滤属性: 哪些字段可以用作过滤器 (
=,>,IN等)。 - 排序属性: 哪些字段可以用来显式排序结果 (
sort参数)。 - 分词器/停用词/同义词: 如何处理文本。
- 排名规则: 决定搜索结果最终排序顺序的核心算法组合。
阶段一:添加/更新文档
-
客户端请求:
- 用户/应用通过 MeiliSearch 的 API(通常是
POST /indexes/{index_uid}/documents)发送一个包含一个或多个 JSON 文档的请求。 - 文档可以包含各种类型的字段:字符串、数字、布尔值、数组、嵌套对象等。
- MeiliSearch 接收请求。
- 用户/应用通过 MeiliSearch 的 API(通常是
-
任务队列:
- MeiliSearch 不会立即处理文档。它将该操作(添加/替换文档)作为一个任务放入其异步任务队列中。
- 客户端立即收到一个包含
taskUid的响应,表示操作已接受,但尚未完成。状态通常是enqueued。
-
任务处理:
- MeiliSearch 的后台工作线程(Worker)从任务队列中取出该任务进行处理。
- 核心操作:索引文档 (Indexing Documents)。这一步涉及深度处理文档内容以构建或更新内部数据结构(索引),使其能被快速搜索。处理过程如下:
- a. 文档解析与字段提取:
- 解析传入的 JSON 文档。
- 提取文档的唯一标识符 (
id)。如果没有提供或无效,操作会失败(除非配置允许自动生成)。 - 根据索引当前的设置,识别文档中的各个字段。
- b. 文本处理与分词 (Tokenization): 这是构建搜索核心的关键步骤。
- 针对每个
可搜索属性的字符串值: - 分词 (Tokenization): 使用配置的分词器(Tokenizer)将文本拆分成更小的单元,称为词元(Tokens)。例如:
- 默认分词器:会将
"The quick brown fox"分词为["the", "quick", "brown", "fox"]。 - 中文分词器:需要特定配置(如启用
jieba),会将"搜索引擎"分词为["搜索", "引擎"]或类似。
- 默认分词器:会将
- 规范化 (Normalization):
- 小写化 (Lowercasing): 默认将所有词元转换为小写(
"Fox"->"fox"),确保搜索不区分大小写。 - 去重音 (De-accentuation):
"café"->"cafe"。 - 删除停用词 (Stop Words Removal): 如果配置了停用词列表(如
"the", "a", "an"),这些词元会被移除。 - 词干还原/词形归并 (Stemming/Lemmatization): 将词元还原到基本形式(
"running"->"run","mice"->"mouse")。MeiliSearch 默认使用轻量级词干提取算法(非完整词形归并)。
- 小写化 (Lowercasing): 默认将所有词元转换为小写(
- 同义词扩展 (Synonym Expansion): 如果配置了同义词(如
"car" => ["auto", "vehicle"]),系统会为每个匹配的词元生成额外的词元。 - 输出: 经过处理后的词元列表。例如,原始文本
"The Amazing Spider-Man 2"经过处理后可能变成词元["amaz", "spider", "man", "2"](注意词干提取的效果)。
- 针对每个
- c. 构建倒排索引 (Inverted Index): 这是搜索引擎的核心数据结构。
- 想象一个巨大的“词典”。
- 对于处理后的每个唯一词元(如
"spider"),索引中会记录一个条目(Posting List)。 - 这个条目包含:
- 包含该词元的文档 ID 列表。
- 该词元在每个文档中出现的次数 (Term Frequency - TF)。
- 该词元在每个文档中出现的位置信息(可选,用于短语搜索和邻近度排序)。
- 例如:
- 词元
"spider"->[ doc123 (TF:3, positions: [5, 20, 45]), doc456 (TF:1, position: [12]) ] - 词元
"man"->[ doc123 (TF:2, positions: [21, 46]), doc789 (TF:1, position: [7]) ]
- 词元
- 这样,给定一个搜索词,引擎可以极其快速地查找到包含该词(或其变体)的所有文档列表。
- d. 存储属性文档 (Document Store):
- 虽然倒排索引能快速找到文档 ID,但它不存储完整的文档内容。
- MeiliSearch 将原始文档(或处理后的结构化版本)存储在另一个称为 Document Store 的数据结构中(通常是一个键值存储,键是
docId)。 - 当需要向用户返回搜索结果时,引擎会从 Document Store 中按
docId检索出完整的文档数据以展示。
- e. 更新其他数据结构:
- 过滤/排序字段: 对于标记为
filterable或sortable的字段(通常是数字、日期、分类字符串),MeiliSearch 会构建专门的数据结构(如位图索引、B树、排序列表)来加速范围查询 (price > 100)、相等查询 (color = "red") 和排序操作 (sort=price:asc)。这些操作不依赖分词。
- 过滤/排序字段: 对于标记为
- a. 文档解析与字段提取:
- 事务性与持久化: 整个索引更新过程通常在事务中进行,确保数据一致性。更新后的索引结构会持久化到磁盘。
-
任务完成:
- 文档处理、索引构建/更新完成后,任务状态标记为
succeeded。 - 客户端可以通过
GET /tasks/{taskUid}查询任务状态。一旦成功,新文档即可被搜索到。 - 如果发生错误(如无效 JSON、缺少
id),任务状态会变为failed,并包含错误信息。
- 文档处理、索引构建/更新完成后,任务状态标记为
阶段二:执行检索
-
客户端请求:
- 用户/应用发送搜索请求(
GET /indexes/{index_uid}/search),通常包含以下关键参数:q: 查询字符串(用户输入的搜索词)。filter: 过滤条件(如price > 100 AND color = 'red')。sort: 显式排序规则(如price:asc)。offset/limit: 分页控制。attributesToRetrieve: 指定返回哪些文档字段。attributesToSearchOn: 指定只在哪些字段中搜索(覆盖设置)。
- 用户/应用发送搜索请求(
-
查询预处理:
- 对查询字符串
q应用与索引时相同的文本处理流程:- 分词:将
q拆分成词元。 - 规范化:小写化、去重音。
- 删除停用词(如果配置)。
- 词干还原。
- 同义词扩展(如果配置且启用)。
- 分词:将
- 例如: 用户输入
"Spider man movie"可能被处理为词元["spider", "man", "movi"]。
- 对查询字符串
-
召回 (Retrieval - 查找候选文档):
- 使用倒排索引:
- 对预处理后得到的每个查询词元(
"spider","man","movi"),在倒排索引中查找其对应的 Posting List(包含该词元的文档 ID 列表及其 TF 等信息)。
- 对预处理后得到的每个查询词元(
- 初步文档集合:
- 将找到的所有 Posting List 中的文档 ID 合并起来(通常是取并集
OR语义:包含任意一个查询词元的文档都被召回)。这是最初的、可能很大的候选文档集合。 - 注意: MeiliSearch 默认是
OR语义。虽然结果会按相关性排序,使得同时包含所有词的文档排前面,但最初召回阶段是宽泛的OR。严格AND(+前缀) 是在后续排序阶段处理的。
- 将找到的所有 Posting List 中的文档 ID 合并起来(通常是取并集
- 使用倒排索引:
-
过滤 (Filtering):
- 如果请求中包含
filter参数:- 系统会解析过滤表达式(如
price > 100 AND (color = 'red' OR color = 'blue'))。 - 利用为
filterable属性构建的专用数据结构(位图索引等),高效地计算出满足过滤条件的文档 ID 集合。
- 系统会解析过滤表达式(如
- 应用过滤:
- 将上一步得到的初步文档集合与过滤条件满足的文档集合进行交集运算。
- 结果是一个过滤后且包含至少一个查询词元的文档 ID 集合。这是进入核心排序阶段的候选集。过滤发生在排序之前,效率极高。
- 如果请求中包含
-
排序 (Ranking - 相关性计算): 这是 MeiliSearch 强大易用性的核心!
- 目标:根据相关性对过滤后的候选文档集合进行排序。
- 机制:流水线式应用一系列预定义的排名规则 (Ranking Rules)。每个规则为每个文档计算一个分数,规则按优先级顺序应用,后面的规则用于打破前面规则产生的平局。默认规则(按优先级从高到低):
- 1. 词法 (Words):
- 考虑因素:词元在文档中的 TF (Term Frequency),词元在查询中的重要性(通常所有词元同等重要),词元在文档字段中的 位置信息(如果存储了)。
- 目标:优先显示包含更多查询词元、查询词元出现频率更高、查询词元出现在更重要位置(如标题)或更靠近开头的文档。
- 关键点: 这是最核心的相关性信号。同时包含所有查询词元的文档在此规则下得分最高。 它实现了默认的“弱AND”语义:包含所有词的文档会排在不包含所有词的文档之前。
- 2. 属性 (Typo):
- 考虑因素:文档中匹配查询词元时存在的拼写错误数量 (Levenshtein Distance)。
- 目标:优先显示包含更精确匹配(拼写错误更少)的文档。对模糊搜索和容错至关重要。
- 3. 临近度 (Proximity):
- 考虑因素:文档中匹配的查询词元之间的物理距离(单词间隔数)。
- 目标:优先显示查询词元在文档中出现位置彼此靠近的文档。例如,搜索
"spider man",标题"Spider-Man: Homecoming"会比正文中分散出现spider和man的文档得分高。
- 4. 属性 (Attribute):
- 考虑因素:匹配发生的字段的重要性(根据
searchableAttributes顺序定义)。 - 目标:优先显示在更重要字段(如
title)中找到匹配的文档,而不是在次要字段(如description)中找到匹配的文档。
- 考虑因素:匹配发生的字段的重要性(根据
- 5. 词位 (Words Position):
- 考虑因素:文档中第一个匹配到的查询词元的位置。
- 目标:优先显示查询词元出现在文档较前位置(如开头)的文档。
- 6. 精确度 (Exactness):
- 考虑因素:词元匹配的精确程度(考虑词干还原/同义词后的原始形式是否完全一致)。
- 目标:优先显示包含与查询词元原始形式更精确匹配的文档。打破前面规则平局的最后手段。
- 1. 词法 (Words):
- 定制化: 用户可以在索引设置中重新排序、添加或删除这些规则,以改变排序优先级。
-
显式排序 (Explicit Sorting -
sort参数):- 如果请求中包含
sort参数(如sort="price:asc"):- 系统会覆盖默认的基于相关性的排名规则排序。
- 利用为
sortable属性构建的专用数据结构(排序列表、B树等),直接按指定字段和顺序(升序asc或降序desc)对过滤后的候选文档集合进行排序。 - 注意:
sort参数优先级最高,会完全忽略相关性排序规则。
- 如果请求中包含
-
分页与结果组装:
- 分页: 根据请求的
offset和limit参数,从最终排序后的文档列表中截取对应的片段(例如,第 11-20 条结果)。 - 获取文档内容: 根据分页后得到的
docId列表,从 Document Store 中检索出这些文档的完整内容(或根据attributesToRetrieve指定的字段子集)。 - 计算估算总数: 计算过滤后符合条件的文档总数 (
estimatedTotalHits)。出于性能考虑,这可能是一个近似值(尤其是结果集非常大时)。 - 生成其他元信息: 如查询处理时间、分页信息、可能的高亮或分词信息(如果请求了
showMatchesPosition等)。
- 分页: 根据请求的
-
返回响应:
- 将组装好的结果(文档列表、分页信息、总数、耗时等)以 JSON 格式返回给客户端。
关键逻辑图示:
[客户端添加文档]
|
v
[任务队列 (enqueued)] --> [Worker 处理任务]
|
v
[文档解析 & 提取 id]
|
v
[ 处理可搜索属性文本:分词 -> 规范化 -> 词干 -> 同义词 ]
|
v
[ 构建/更新倒排索引 (词元 -> docId, TF, 位置) ]
|
v
[ 存储原始文档 (Document Store: docId -> Doc) ]
|
v
[ 更新 Filterable/Sortable 字段索引 ]
|
v
[任务完成 (succeeded)]
|
|
| (文档可被搜索)
|
v
[客户端发送搜索请求 (q, filter, sort)]
|
v
[查询预处理 (分词, 规范化...)]
|
v
[倒排索引召回 (获取候选 docIds)] --------+
| |
v |
[应用过滤器 (filter 参数)] <----[Filterable 索引]
| |
v |
[排序阶段] |
| |
+-----------------------|-------------------------+
| | |
| [相关性排序 (Ranking Rules)] [显式排序 (sort 参数)] <----[Sortable 索引]
| | |
| v v
| [取排序靠前的文档] [按指定字段排序]
| | |
+---------------------->|<------------------------+
|
v
[分页 (offset/limit)]
|
v
[从 Document Store 获取文档内容]
|
v
[组装响应 (结果, 分页, 总数...)]
|
v
[返回结果给客户端]
总结与关键点:
- 异步写入: 添加/更新文档是异步任务,通过队列处理,确保高吞吐量和响应性。
- 文本处理一致性: 索引时对文档文本的处理(分词、规范化、词干等)与搜索时对查询字符串的处理必须完全一致,否则无法正确匹配。
- 倒排索引是核心: 实现快速文本搜索召回的基础。
- 专用数据结构:
filterable和sortable属性使用位图、B树等结构,使得过滤和显式排序极其高效,避免全表扫描。 - 过滤优先: 过滤在相关性排序之前应用,大幅减少需要排序的文档数量,提升性能。
- 排名规则流水线: 相关性排序由一系列明确定义的规则按优先级顺序应用完成。默认规则提供了开箱即用的优秀相关性。
words规则实现了默认的弱AND语义。 sort参数优先级最高: 显式排序会完全覆盖默认的相关性排序。- Document Store 分离: 倒排索引存储映射关系,Document Store 存储原始数据,优化了存储和检索效率。
- 配置驱动: 索引的
settings(可搜索属性、过滤属性、分词、停用词、同义词、排名规则顺序) 对搜索行为有决定性影响,需要根据数据特性和搜索需求仔细调优。
理解这些内部逻辑,能帮助你更好地设计数据结构、配置索引设置、编写高效的查询以及诊断可能遇到的问题。MeiliSearch 的设计在易用性(开箱即用的优秀相关性)和灵活性(可配置的设置)之间取得了很好的平衡。