
简介RediSearch 是构建于 Redis 之上的高性能全文搜索引擎模块该资源包面向需要为实时应用、内容站点或电商系统引入搜索能力的开发者也适合希望深入理解倒排索引与检索实现的进阶读者。包内提供完整源码与工程配置可通过相应命令快速搭建全文检索、过滤、排序及地理空间查询能力。资源共 1002 个文件压缩包约 4.79MB主体为 C/C 实现同时包含 Python 脚本、YAML 配置和 Markdown 文档压缩包内目录结构按源码、测试、文档划分便于查找使用源码中还能看到分词词库、词干分析、压缩库等底层模块并配有测试用例与构建辅助文件便于本地编译与二次开发。目前已有 518 人浏览学习读者可借此了解 RediSearch 的索引机制、查询语法与模块加载方式既能作为学习搜索引擎原理的实例也能直接作为项目集成或二次开发的起点。1. 把缓存盘成搜索引擎RediSearch 模块的选型边界Redis 最常见的归宿是缓存和队列但把 Redis 只当缓存用等于放弃了它最被低估的能力——全文搜索。RediSearch 是跑在 Redis 进程内的搜索模块装上之后FT.CREATE、FT.SEARCH、FT.AGGREGATE 这一组命令就是一套完整的搜索引擎倒排索引、分词、数值/标签/地理过滤、分面统计、相关度排序全都有。它适合的场景很明确数据量在百万到千万级、并发高、实时性要求苛刻的站内搜索不想为此单独维护一套 Elasticsearch 集群。拿到这套基于 Redis 的全文搜索引擎源码先别急着跑构建把模块的 C 源码翻一遍你会比只背命令的人多理解一层索引机制后面调参和排错都会顺手很多。2. 从模块源码逆推倒排索引、多语言词干与查询解析2.1 源码包里藏着什么解开源码包能看到build-redisjson、games.json.bz2、cndict_data.c、_ducet.c、stem_UTF_8_serbian.c、stem_UTF_8_greek.c、lemon.c、miniz.c、spec.c这一组文件。build-redisjson负责把原始 JSON 数据编译成评测数据集games.json.bz2是自带压测数据spec.c是模块的回归测试规格。真正决定搜索能力的是剩下几个 C 文件词干提取器、Unicode 排序表、语法解析器和压缩库。把文件按职责归类就能倒推出 RediSearch 的核心架构分词和语言处理由词干文件加 DUCET 表完成查询语法由 lemon 生成的解析器处理倒排索引直接构建在 Redis 的数据结构之上压缩层用 miniz 处理数据集和索引块。搞清楚这条链路后面遇到中文搜不到、重音字符匹配不上这类问题你就能直接定位到是哪一层出了问题。2.2 倒排索引的数据组织全文搜索的灵魂是倒排索引。正向索引是文档 - 词项倒排索引反过来存词项 - 文档列表。搜索redis时引擎直接去词项表里查拿到包含该词的文档 ID 列表再按相关度排序返回而不是逐条扫描所有文档。在 RediSearch 里这张倒排表不是单独存在某个文件里而是拆分进多个 Redis key。索引元数据、词项倒排列表、文档字段值、地理单元、标签集合各有各的键空间命名大致以idx:索引名开头。可以用redis-cli --scan --pattern idx:*观察模块实际创建了哪些键但注意内部键后缀会随版本变化生产环境不要对这些键做硬编码依赖。提示倒排列表在 RediSearch 中会按字块压缩存储块大小可以用FT.CREATE的INDEXOPTIONS调默认值在读写吞吐上比较平衡。高频写入场景下把块调大能减少内存碎片。2.3 词干提取与 Unicode 排序规则stem_UTF_8_serbian.c、stem_UTF_8_greek.c这类文件一看命名就知道是 Snowball 词干算法的 C 语言生成产物。词干的作用是把同一单词的不同形态归并搜索running能同时命中run和runs。RediSearch 把每种语言编成独立的 C 文件编译时按需要启用这就是它能在多语言场景下保持索引体积可控的原因。如果业务涉及塞尔维亚语或希腊语确认对应词干文件被编进了模块如果只用中英文这些文件不参与编译反而能减少模块体积。_ducet.c是 Unicode Collation Element Table负责大小写折叠和重音字符归一。它的存在意味着café和cafe在索引层会被当成相近的词项处理这对国际化搜索体验是硬需求。在 FT.CREATE 的 SCHEMA 里给字段加WITHSUFFIXTRIE或调NOSTEM决定的是是否跳过词干化而 DUCET 表是全局生效的改不了只能理解它。2.4 查询语法解析与压缩层lemon.c是 LALR(1) 语法分析器生成器SQLite 用的就是它。RediSearch 用 lemon 生成查询语法解析器所以你能写出title:(foo|bar) -year:[2020 2024]这种嵌套布尔表达式解析器会把它编译成一颗可执行的查询树。这也是 RediSearch 能做短语查询、附近查询、通配符查询而不容易出解析漏洞的原因。miniz.c是 zlib 兼容的压缩库。它一方面用于解压games.json.bz2这类评测数据集另一方面为倒排索引提供压缩存储。索引压缩默认开启代价是查询时多一次解压但换来的内存节省非常可观尤其是文本字段多的索引。2.5 模块与 Redis 数据结构的映射RediSearch 的文档不是独立存储格式而是直接复用 Redis 的 Hash。一个 Hash key 对应一篇文档文档字段就是 Hash 的 field。这意味着你可以继续用HSET写入数据用TTL控制过期用EXPIRE做清理搜索层和数据层共用同一份数据。映射关系如下数据层面Redis 实现RediSearch 对应文档Hash key一个 docid字段值Hash fieldSCHEMA 里声明的属性倒排列表Redis String/List 编码词项到 docid 的映射文档过期TTL 机制索引自动剔除过期文档持久化RDB/AOF模块数据随 Redis 一起落盘TTL 过期删除在 RediSearch 里是惰性感知的文档过期后搜索时会被跳过但倒排索引里的残留记录要等索引清理任务回收。这块机制决定了你不能把 RediSearch 当 Elasticsearch 的重删场景用频繁写又频繁过期会导致索引膨胀。3. FT.CREATE 建索引与 FT.SEARCH 首次查询从模块加载到返回结果3.1 加载模块的两种方式拿到模块编译产物后最省事的验证方式是直接用 Redis Stack 镜像它内置 RediSearch。如果要在已有 Redis 实例上加载启动命令里加--loadmodule指向模块路径即可。# 方式一官方 Redis Stack 镜像自带 RediSearch 和 RedisJSON docker run -d --name redis-search -p 6379:6379 redis/redis-stack-server:latest # 方式二在已有 Redis 6.0 上加载模块 redis-server --port 6379 --loadmodule /opt/redisearch/redisearch.so # 方式三写进 redis.conf重启自动加载 echo loadmodule /opt/redisearch/redisearch.so /etc/redis/redis.conf redis-server /etc/redis/redis.conf参数说明--loadmodule后面跟模块.so文件的绝对路径方式三更适合生产环境配置持久化、主从复制时不会因为启动参数遗漏导致模块没加载。加载完成后用MODULE LIST确认redis-cli MODULE LIST # 1) name - search version - 20804看到search模块在列表里就可以建索引了。我一般建议在主从架构里让所有节点加载同一版本的模块否则从节点同步索引数据时会报模块版本不匹配的错误。3.2 FT.CREATE 定义索引结构RediSearch 的索引必须显式声明字段类型决定你能对它做什么操作。比如TEXT支持全文搜索和模糊匹配TAG支持精确等值匹配但走的是标签倒排NUMERIC和GEO分别支持范围过滤和地理半径过滤VECTOR支持向量相似度搜索。FT.CREATE idx:games ON HASH PREFIX 1 game: SCHEMA \ name TEXT WEIGHT 5.0 \ detail TEXT NOSTEM \ platforms TAG SEPARATOR , \ tags TAG SEPARATOR , \ price NUMERIC \ rating NUMERIC \ location GEO命令逻辑说明ON HASH表示索引作用在 Hash 类型上PREFIX 1 game:限定只索引game:前缀的 key避免把业务里其他无关 Hash 也卷进来。WEIGHT 5.0表示 name 字段的相关度权重是默认字段的 5 倍搜索时标题命中比正文命中排得更前。NOSTEM让 detail 字段不做词干化适合禁止形态归并的专有名词文本。字段类型的选择直接决定索引体积字段类型典型用途索引开销支持查询TEXT标题、正文、描述最高全文、短语、模糊、通配符TAG标签、分类、枚举低精确等值、多值匹配NUMERIC价格、时间戳、数值低范围、排序、聚合GEO经纬度坐标低半径搜索、按距离排序VECTOR嵌入向量中KNN 相似度搜索3.3 写入文档并执行第一次搜索索引建好后文档不需要额外 API直接用HSET写入带前缀的 Hash key。这里用 Python 的redis-py演示完整流程import redis r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) # HSET 写入一篇文档field 名称必须和 SCHEMA 声明一致 r.hset(game:1001, mapping{ name: The Legend of Zelda: Breath of the Wild, detail: Open world action adventure game, platforms: Switch,WiiU, tags: adventure,puzzle,openworld, price: 59.99, rating: 4.9, location: 35.67,139.65 }) # 执行全文搜索在 name 字段中查 zelda返回相关度打分后的结果 res r.execute_command( FT.SEARCH, idx:games, name:zelda, RETURN, 3, name, price, platforms ) print(res)这里execute_command是通用命令通道FT.*命令还没有全部封装进redis-py的 ORM 接口直接用原始命令最稳。RETURN 3 name price platforms指定返回字段减少网络传输量。搜索时如果只写zelda而不是name:zeldaRediSearch 会按默认字段没有指定则所有 TEXT 字段横跨搜索未指定默认字段的索引会报错提示你配置DEFAULT_FIELD。3.4 理解返回结构FT.SEARCH的返回结果是一个扁平数组嵌套层级固定解析时按位置取数1) 1 # 命中总数 2) game:1001 # 文档 key 3) 1) name # 字段名 2) The Legend of Zelda: Breath of the Wild # 字段值 3) price 4) 59.99 5) platforms 6) Switch,WiiU第一层是命中条数之后每两个元素一组文档 key、字段名/字段值对。加了WITHSCORES会在文档 key 后多一个相关度分值。解析时建议写成循环而不是硬编码下标因为RETURN字段增删会改变数组形状。提示索引不存在的查询立刻返回ERR Unknown Index name字段名写错返回Unknown field。这两种错误信息是定位问题最快的信息源。4. 复杂查询、过滤排序与 FT.AGGREGATE 分面统计实战4.1 准备评测数据集源码包里自带games.json.bz2这是模块官方测试用的游戏数据集包含名称、平台、标签、价格、评分等字段天然适合演练过滤和聚合。先解压并用脚本灌进 Redisbzip2 -dk games.json.bz2import json, redis r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) with open(games.json, r, encodingutf-8) as f: data json.load(f) # 兼容 JSON 数组和单对象两种格式按实际文件结构调整 items data if isinstance(data, list) else [data] for g in items: key fgame:{g.get(id)} mapping { name: g.get(name, ), detail: g.get(detail, ), # TAG 字段用逗号连接对应 FT.CREATE 里的 SEPARATOR , platforms: ,.join(g.get(platforms, [])), tags: ,.join(g.get(tags, [])), price: g.get(price, 0), rating: g.get(rating, 0), } # 存在经纬度字段时写入 GEO 字段 if g.get(longitude) is not None and g.get(latitude) is not None: mapping[location] f{g[longitude]},{g[latitude]} r.hset(key, mappingmapping)这段脚本把 JSON 数据转成 Hash 写入 Redis。注意 TAG 字段必须按SEPARATOR指定的分隔符拼成一个字符串否则标签匹配会失效。GEO 字段的标准格式是经度,纬度不要写反。灌完数据用FT.INFO idx:games看num_docs是否等于导入条数。4.2 查询语法进阶FT.SEARCH的查询语法在普通关键词之上还支持前缀、模糊、短语、布尔组合。下面的命令一次覆盖了几种高频查询形态# 布尔组合必须含 adventure 或 puzzle但不能是 openworld 标签 FT.SEARCH idx:games (tags:{adventure} | tags:{puzzle}) -tags:{openworld} # 前缀匹配name 以 zel 开头限制返回 10 条 FT.SEARCH idx:games name:zel* LIMIT 0 10 # 模糊匹配与 zilda 编辑距离在 2 以内的词都会被命中 FT.SEARCH idx:games name:%%zilda%% # 短语搜索detail 字段中 open world 必须按顺序相邻出现 FT.SEARCH idx:games detail:\open world\语法逻辑说明|表示或-表示排除*是通配符%%包裹的词启用 Levenshtein 模糊匹配双引号包裹的内容做短语匹配。模糊匹配在索引大、词项多时开销明显我一般在搜索建议场景才会用它常规搜索建议用前缀匹配更高效。通配符同理foo*能复用前缀 trie而*foo会触发全表扫描务必避免。4.3 过滤、排序与分页数值范围和地理位置过滤可以叠加在全文条件上这是 RediSearch 相比普通 Redis 查询最实用的能力# 价格在 20 到 50 美元之间评分不低于 4.5按价格降序 FT.SEARCH idx:games tags:{rpg} price:[20 50] rating:[4.5 inf] \ SORTBY price DESC LIMIT 0 20 # 以东京站为圆心100 公里半径内的游戏按距离升序 FT.SEARCH idx:games location:[139.65 35.67 100 km] \ SORTBY location ASC PARAMS 2 lon 139.65 lat 35.67 LIMITED参数说明price:[20 50]是闭区间inf表示正无穷SORTBY price DESC按字段排序支持多个字段连续排序GEO 过滤的半径单位可以用m、km、mi、ft。GEO 排序需要配合PARAMS传参考点坐标LIMITED在这里是为了防止全量距离计算注意这个参数对排序准确性有取舍。4.4 FT.AGGREGATE 分面统计分面导航是电商搜索的标配能力搜完关键词还要统计每个分类下的商品数量。FT.AGGREGATE就是为此设计的语法类似 SQL 的GROUP BY但不用建宽表。# 统计每个平台的游戏数量取 Top 5 FT.AGGREGATE idx:games * \ GROUPBY 1 platforms \ REDUCE COUNT 0 AS platform_total \ SORTBY 2 platform_total DESC \ LIMIT 0 5 # 统计价格区间分布每 10 美元一个桶 FT.AGGREGATE idx:games * \ APPLY floor(price / 10) * 10 AS price_bucket \ GROUPBY 1 price_bucket \ REDUCE COUNT 0 AS cnt \ SORTBY 2 price_bucket ASC第一条GROUPBY 1 platforms按平台字段分组REDUCE COUNT 0 AS platform_total统计每组文档数并把结果命名为platform_totalSORTBY 2 platform_total DESC表示按该字段降序排序这里的2是排序字段加方向的参数数量。第二条APPLY先对price做算术运算生成price_bucket再对桶分组计数实现了价格直方图效果。聚合查询比FT.SEARCH更接近 OLAP 的使用方式适合做报表接口。需要注意REDUCE支持COUNT、SUM、AVG、MIN、MAX、QUANTILE、STDDEV等聚合函数应用到数值字段时要确保字段类型是NUMERIC否则返回空值且不报错。5. 中文分词、字典资源与生产排错5.1 中文分词现状和应对RediSearch 默认分词基于 ICU 的 Unicode 文本切分按空格和标点把文本切成词项。这个机制对英文友好但中文文本没有词边界一段「塞尔达传说旷野之息」会被整个存成一个词项搜「传说」匹配不到全文。这是中文用户最常栽的坑。工程上的常见做法是写入前先分词把分词结果冗余到一个专用字段里。以 jieba 为例import jieba text 塞尔达传说旷野之息 # 用空格分隔的分词结果让 RediSearch 按空格切分成多个词项 tokens .join(jieba.cut(text)) print(tokens) # 塞尔达 传说 旷野 之 息写入时把tokens存进search_text字段查询时搜search_text:传说。源码包里的cndict_data.c就是内置中文词典数据的编译产物为中文分词扩展提供了词频和词性基础。生产上做中文搜索我建议在应用层统一处理分词而不是完全依赖服务端分词配置因为查询端的输入也要走同样的分词逻辑否则写入和检索两套规则不一致召回率会大幅波动。5.2 索引质量与持久化检查在手工压测或接流量前先把基线指标捞出来FT.INFO idx:games重点看num_docs、num_terms、records_per_doc_avg、index_size这几个值。index_size除以num_docs可以得到单文档平均索引开销如果这个数值异常偏大说明字段类型声明不合理——比如把本应是 TAG 的枚举字段声明成了 TEXT倒排表被撑爆。持久化方面RediSearch 数据跟随 Redis 的 RDB/AOF 走RDB 保存时模块数据一并序列化AOF 重写时以模块指令形式记录。注意两件事从节点必须加载相同版本的模块AOF 开启时尽量保持默认配置不要自定义aof-rewrite-incremental-fsync这类参数否则极端崩溃场景下可能丢索引段。5.3 三类高频报错的处理现象根因处理方式ERR Unknown Index name索引未创建或创建失败FT._LIST查看全部索引确认名称拼写Unknown fieldSCHEMA 里没声明该字段查询字段名和创建索引的 SCHEMA 逐一比对搜索得到空结果但文档已存在PREFIX 前缀没对上keys game:*确认 key 是否带索引前缀最后一个不容易想到的坑是内存淘汰策略。如果 Redis 配置了allkeys-lru或volatile-lru索引内部 key 可能被淘汰引擎误杀表现为文档还在但搜不到。给 RediSearch 实例设置maxmemory-policy noeviction让搜索引擎的键空间不被普通缓存键挤占索引数据要过期就用文档级 TTL密钥级 TTL 的随机淘汰会破坏倒排索引的完整性。模块上线前把maxmemory预算算好索引大约占源数据的三分之一到一半给足余量再切流量。本文还有配套的精品资源点击获取