资讯动态

Apache CouchDB Nouveau:基于 Lucene 的库级全文搜索从入门到源码剖析

发布时间:2026/10/9 1:47:12 来源:尧图企业网站定制
数据库文档数据库后端【免费下载链接】couchdbSeamless multi-primary syncing database with an intuitive HTTP/JSON API, designed for reliability项目地址https://gitcode.com/gh_mirrors/co/couchdb点击查看免费下载Nouveau 是 Apache CouchDB 中用于替代 dreyfus/clouseau 的新一代实验性全文搜索模块它把 CouchDB 的每个数据库分片shard转换为一个独立的 Apache Lucene 索引再将各分片索引的查询结果合并返回。本文基于 nouveau/README.md 完整梳理其能力边界、构建运行步骤、索引函数 API 与查询参数并结合 Java 端与 Erlang 端源码剖析其分片级索引管理、书签分页与陈旧索引重试等底层机制。1. 定位与技术栈dreyfus/clouseau 的现代替代README 对 Nouveau 的定位非常明确它构建在三样技术之上见 nouveau/README.mdDropwizard 框架一个 Java 后端应用框架NouveauApplication.java 即其应用入口Java 11Lucene 9。其核心思路是Nouveau 在分片shard层面把 CouchDB 数据库转换为 Lucene 索引然后把各分片索引的查询结果合并在一起。这一定位与旧方案 dreyfus/clouseau基于 Scala 的 clouseau 库形成对照。注意README 明确声明该工作目前仍处于 EXPERIMENTAL实验阶段可能会以破坏已有 Nouveau 索引兼容性的方式发生变化。在生产环境使用前需评估这一点。1.1 为什么优于 dreyfus/clouseauREADME 给出的四点理由原文逐条对应不再需要 scalang也不需要 Scala支持 Lucene 9 支持的任意 Java 版本使用内存映射 I/Omemory-mapped I/O提升性能README 指出在 Java 19 下效果最佳段segment合并时使用 direct I/O从而避免把有用的数据从磁盘缓存中逐出。其中direct I/O 用于段合并这一点可以从源码得到印证IndexManager.java 在加载索引时使用的正是 Lucene misc 包提供的DirectIODirectory包裹FSDirectoryfinal Directory dir new DirectIODirectory(FSDirectory.open(path.resolve(9)));1.2 能力现状README 官方清单README 用两节清单精确划定了当前能力边界这是使用 Nouveau 前必须了解的契约已经可用的功能What works?可定义默认分析器default analyzer并可为不同字段名指定不同分析器支持对文本和数字字段以及多字段组合排序支持经典 Lucene 查询语法支持 count字符串计数与 range数值范围两种 facet书签bookmark支持可高效分页遍历大结果集数据库被删除时索引自动删除前提是 nouveau 正在运行与 ken数据库检查点管理模块集成与 mango 集成与 resharding重分片集成updatefalse查询选项_nouveau_info接口README 中列出的_search_cleanup当前源码中清理处理器实际注册在_nouveau_cleanup路径见第 7 节/openapi.{json,yaml}OpenAPI 描述端点。尚未可用的功能What doesnt work yet?结果分组results grouping——README 说明作者不打算添加分组支持因为似乎几乎没人用但欢迎整洁的贡献分析器的可配置停用词stop wordsMakefile.win或者说 Windows 平台整体。2. 入门配置、构建与启动README 的 Getting started 给出了三步走流程下面结合仓库中的构建脚本与配置默认值补充细节。2.1 用 --with-nouveau 配置 CouchDB./configure --with-nouveauconfigure 脚本中该开关的说明是 build the new experimental search module它对应 Makefile 变量with_nouveau。当with_nouveau true时Makefile 的nouveau目标会执行cd nouveau ./gradlew spotlessApply cd nouveau ./gradlew build -x test也就是说Nouveau 的 Java 服务端通过 Gradle 构建见 nouveau/gradlew。configure 同时为local.ini写入一组默认配置见 configure{nouveau_index_dir, ./data/nouveau} % 索引存储根目录 {nouveau_url, http://127.0.0.1:5987} % CouchDB 访问 Java 服务的地址 {nouveau_port, 5987} % 服务端口 {nouveau_admin_port, 5988} % 管理端口Dropwizard 健康检查等另外Erlang 侧所有 HTTP 处理器都会先做check_if_enabled()检查该检查读取配置项[nouveau] enable默认值为 false见 nouveau.erl 与 nouveau_httpd.erl。当开关未打开时搜索相关请求会直接返回 404这一点在联调时容易被误判为路径写错。2.2 构建与运行# 构建 Nouveau即 make nouveau内部调用 gradlew make # 启动 CouchDB 并启用 nouveau dev/run --adminfoo:bar --with-nouveaudev/run 启动脚本的--with-nouveau参数会拉起 CouchDB 与独立运行的 Nouveau Java 进程。两者是分离的Erlang 侧的 src/nouveau 模块通过 HTTP 与 Java 服务通信因此索引目录、URL 和端口都要按 2.1 的配置正确指向 Java 服务。3. 完整示例建库、建索引、写入与查询README 提供了一个可直接运行的 shell 脚本这里完整保留并逐段解释#!/bin/sh URLhttp://foo:bar127.0.0.1:15984/foo curl -X DELETE $URL curl -X PUT $URL?n3q16 curl -X PUT $URL/_design/foo -d {nouveau:{bar:{default_analyzer:standard, field_analyzers:{foo:english}, index:function(doc) { index(\string\, \foo\, \bar\); }}}} # curl $URL/_index -Hcontent-type:application/json -d {type:nouveau, index: {fields: [{name: bar, type:number}]}} for I in {1..5}; do DOCID$RANDOM DOCID$[ $DOCID % 100000 ] BAR$RANDOM BAR$[ $BAR % 100000 ] curl -X PUT $URL/doc$DOCID -d {\bar\: $BAR} done while true; do curl foo:barlocalhost:15984/foo/_design/foo/_nouveau/bar?q*:* done各步骤要点PUT /foo?n3q16创建 3 副本N、16 个分片区间Q的数据库即共 48 个分片。Nouveau 会为每个副本的每个分片区间各建一个 Lucene 索引README 原文This will cause Nouveau to build indexes for each copy (N) and each shard range (Q) and then perform a search and return the results。设计文档_design/foo中的索引定义由两部分组成分析器配置default_analyzer: standard指定默认分析器field_analyzers: {foo: english}为字段foo单独指定英文分析器。这与 IndexDefinition.java 的 JSON 结构一一对应该类使用JsonNaming(SnakeCaseStrategy)且default_analyzer标注NotEmpty必填field_analyzers为字段名到分析器名的映射索引函数index: function(doc) { index(\string\, \foo\, \bar\); }一段 JS 代码把文档字段bar的值以string类型写入名为foo的索引字段。注释掉的_index请求展示了 Mango 风格的等价定义方式type: nouveau对应 README 中与 mango 集成的声明。循环写入 5 个仅含bar数值的文档。最后的轮询循环反复发起搜索q*:*是 Lucene 的匹配全部查询。README 说明这个路径之所以是_nouveau而不是 dreyfus 的_search是为了避免与 dreyfus 冲突In order not to collide with dreyfus Ive hooked Nouveau in with new paths。该路径映射由 nouveau_httpd_handlers.erl 注册设计文档级处理器_nouveau搜索与_nouveau_info索引信息。4. 搜索接口查询参数、排序与 Facet4.1 请求与响应结构搜索入口在 nouveau_httpd.erlGET /{db}/_design/{ddoc}/_nouveau/{index}也支持 POST请求体为 JSON。Erlang 侧对查询参数做了完整校验其规则validate_query_arg/2可归纳为下表参数说明默认值/约束qLucene 查询语句支持经典 Lucene 查询语法必填缺失时报q parameter is mandatorylimit返回的 hit 数默认 25必须为正整数sort排序字段列表字符串数组可选counts字符串字段计数 facet 的字段名列表可选必须是字符串列表ranges数值字段范围 facet字段名 → 范围对象列表可选值必须是对象列表update查询时是否允许索引更新默认true传false可禁止README 声明已支持bookmark上一次响应的书签用于翻页可选include_docs是否随 hit 返回完整文档默认falsepartition分区键过滤分区数据库可选locale查询解析使用的区域设置可选成功响应体固定包含见 handle_search_req/6bookmark、total_hits、total_hits_relation、hits、counts、ranges后两者无对应 facet 时为 null。其中hits在include_docstrue时会经 include_docs/3 通过fabric:all_docs回填完整文档。值得注意的健壮性设计当 Java 侧报告service_unavailable典型场景是索引落后于数据库的 update_seq见第 6 节时Erlang 侧会最多重试 20 次、每次间隔 500ms?RETRY_LIMIT, ?RETRY_SLEEP见 nouveau_httpd.erl而不是把错误直接抛给客户端。4.2 排序语法README 给出sort[fieldnameherestring]或sort[fieldnameherenumber]并注明默认按 number即可省略类型后缀。源码侧的对应实现在 Lucene9Index.convertSortField/1排序项用正则^([-])?([\.\w])$解析-/前缀表示降/升序特殊值relevance映射到 Lucene 的SortField.FIELD_SCORE按相关度字段类型取自索引 schemaSTRING类型用SortedSetSortFieldDOUBLE类型用SortedNumericSortField若某个分片内没有任何文档带有该排序字段源码会退化为UnknownSortField——从注释看该比较器认为所有文档在此字段上取值相同全 null从而保证多分片合并排序时结果仍可按后续键继续比较默认排序为相关度 _idDEFAULT_SORT见 Lucene9Index.java且 toSort/1 会在用户排序字段末尾自动追加_id以确保分片内排序稳定——这是书签分页正确性的基础。4.3 Facetcount 与 rangeREADME 的示例注意-g让 curl 不转义 URL 中的{}curl foo:barlocalhost:15984/foo/_design/foo/_nouveau/bar?q*:*limit1ranges{bar:[{label:cheap,min:0,max:100}]}counts[foo] -g即字符串字段做计数、数值字段做范围。Java 端实现见 Lucene9Index.collectFacets/4counts走 Lucene facets 模块的StringValueFacetCounts基于 DocValuesranges走DoubleRangeFacetCounts范围对象支持label、min、max以及 DoubleRange.java 中的min_inclusive/max_inclusive开闭区间选项缺省边界取 ±无穷。4.4 书签分页高效遍历大结果集README 将 bookmark support for paginating efficiently through large results sets 列为已支持能力。其机制是每个 hit 携带一个after游标各排序键在该 hit 上的取值见 Lucene9Index.toAfter/1客户端把响应中的bookmark原样回传即可取下一页。hitCollector/1 把after还原为 Lucene 的FieldDoc交给TopFieldCollectorManager从该点之后继续收集——这是一种按排序位置游标翻页而非按偏移量翻页的方案避免了深分页deep paging重复扫描的问题。Erlang 侧 nouveau_bookmark.erl 负责书签的打包与解析。5. 索引函数 APIREADME 的 Index function 表格定义了索引函数中index(...)的四种类型完整继承如下调用形式效果index(text, foo, bar, {store: true});对值做全文分析分词用于全文检索可选保存原值index(string, foo, bar, {store: true});把值作为单个 token整词索引可选保存原值index(double, foo, 12.0, {store: true});索引数值可选保存原值index(stored, foo, bar);仅存储数值随 hit 一起返回不可搜索index(stored, foo, 12.0);仅存储字符串随 hit 一起返回不可搜索这些类型与 Java 端 Lucene9Index.toDocument/2 的落地实现一一对应API 类位于 api 包TextField、StringField、DoubleField、StoredFieldtext→ LuceneTextField经过分析器分词支持全文检索string→ LuceneKeywordField整词索引不切分适合精确匹配与 facetdouble→ LuceneDoubleField数值索引支持范围查询与数值排序stored→ LuceneStoredField只存不索引值会随命中结果返回支持字符串、数值字节数组会尝试按 UTF-8 解码失败则存二进制。两个实现细节值得注意store选项控制Store.YES/NO此外源码中下划线前缀的字段名被保留索引函数若写出_xxx字段会被直接跳过见 toDocument/2 中 Underscore-prefix is reserved 的注释——_id由系统自动写入并作为去重与分页键。6. 分片级架构一个.couch文件对应一个 Lucene 索引Nouveau 的核心抽象是单个分片的 Lucene 索引抽象基类 Index.java 的 Javadoc 精确定义了它的语义索引reflects a single.couchfile shard of some database反映某个数据库的单个.couch分片文件只允许顺序修改updates and deletes但允许多个并发搜索要求每次修改附带单调递增的更新序号。6.1 序列号seq协议与陈旧索引保护Index.java 中update/2与delete/2都是synchronized方法并分别维护updateSeq与purgeSeq两条序列每次写入必须携带matchSeq期望的当前值与seq新值若乱序则抛出UpdatesOutOfOrderException。而search/1入口会先执行 assertMinSeqs/2如果调用方要求的minUpdateSeq/minPurgeSeq高于索引当前进度就抛出StaleIndexException。这正是第 4.1 节中 Erlang 侧收到service_unavailable并重试的根源——它保证了搜索不会读到落后于请求一致性的索引状态而是短暂等待索引追上。6.2 索引生命周期LRU 缓存、定时提交与目录布局IndexManager.java 的 Javadoc 称其为 The central class of Nouveau, responsible for loading and unloading Lucene indexes and making them available for query。关键机制目录布局每个索引位于{rootDir}/{name}/下含index_definition.json索引定义见 indexDefinitionPath/1与index/目录name会做路径逃逸检查防止../越权见 indexRootPath/1LRU 缓存maxIndexesOpen个索引同时打开超容量时按最近最少使用逐出LRUMap继承LinkedHashMap见 IndexManager.java 与 evictIfOverCapacity/0定时提交每个已加载索引都会注册一个周期任务每commitIntervalSeconds秒调用一次commit()Lucene9Index.doCommit/2 把当前update_seq、purge_seq和_schema写入 Lucene 的 live commit data重启后由 IndexManager.getSeq/2 从getLiveCommitData()恢复——这就是单调序列号能跨进程持久化的方式索引定义幂等创建create/2用临时文件 ATOMIC_MOVE落盘定义若同名索引已存在且定义不同则返回 417Index already exists见 assertSame/2。6.3 数据库删除时的索引自动清理README 声明indexes automatically deleted if database is deleted (as long as nouveau is running!)。对应机制IndexManager.deleteAll/2 会遍历索引根目录下所有含index_definition.json的子目录并删除支持排除列表删除前先unload(name, true)关闭加载中的索引再清理残留的空目录。Erlang 侧的清理入口是POST /{db}/_nouveau_cleanupnouveau_httpd_handlers.erl 注册handle_cleanup_req/2 实现返回 202README 的清单中写作_search_cleanup两者指同一功能以源码注册路径为准。7. 管理端点与运维集成_nouveau_infoGET /{db}/_design/{ddoc}/_nouveau_info/{index}返回索引元信息底层 Index.info/0 汇总updateSeq、purgeSeq、numDocs与磁盘占用Lucene9Index.doDiskSize/0 累加 Directory 下所有文件的长度_nouveau_analyzePOST /_nouveau_analyze可离线调试分析器分词结果handle_analyze_req/1用于验证default_analyzer/field_analyzers配置是否符合预期统计指标每次搜索都会更新nouveau.active_searches计数器与nouveau.search_latency直方图handle_search_req/3可通过 CouchDB 的_stats观测ken / mango / resharding 集成README 将这三项列入已支持清单意味着索引能跟随数据库检查点、Mango 索引管理与分片重分布等生命周期事件正确演进这部分逻辑分布在 src/nouveau/src 下的nouveau_plugin_couch_db.erl、nouveau_index_updater.erl等模块中。8. 部署选项一台 Java 服务可服务整个集群README 最后的 Deployment options 给出两条部署路线均源自分片索引命名的设计All indexes are prefixed with their erlang hostname so you can deploy a single nouveau server per cluster if this meets your needs. You can also configure a different nouveau server for each couchdb node too.There is no need to co-locate the nouveau server with the couchdb cluster, though this is a common option.即所有索引名以其所在 Erlang 节点的主机名为前缀因此一个 Nouveau 服务可以服务整个集群按主机名前缀区分各节点的索引也可以为每个 CouchDB 节点配置独立的 Nouveau 服务Nouveau 服务与 CouchDB 集群无需同机部署只是同机部署更为常见。结合 2.1 的nouveau_url配置每个节点可以指向各自的 Nouveau 实例。9. 小结与适用边界回到 nouveau/README.md 的主线Nouveau 提供了一条分片级 Lucene 索引 集群结果合并的全文搜索路径用 JS 索引函数声明字段类型text/string/double/stored用经典 Lucene 语法、排序、count/range facet 与书签分页消费结果构建上只需configure --with-nouveaumakedev/run --with-nouveau运维上依赖 update_seq/purge_seq 序列协议保证一致性并通过 LRU 缓存与定时提交控制资源。它的当前边界同样清晰无结果分组、无自定义停用词、无 Windows 支持且整体仍属实验特性索引格式可能随版本变化而不兼容——这三点均出自 README应作为采用决策的主要约束。赞分享数据库文档数据库后端【免费下载链接】couchdbSeamless multi-primary syncing database with an intuitive HTTP/JSON API, designed for reliability项目地址https://gitcode.com/gh_mirrors/co/couchdb点击查看免费下载相关推荐CouchDB-Lucene强大的全文搜索解决方案CouchDB Lucene强大的全文搜索解决方案 项目介绍 CouchDB Lucene 是一个开源的全文搜索解决方案专为 Apache CouchDBPlaynite便携版如何轻松更新3步解决游戏库管理难题Playnite便携版如何轻松更新3步解决游戏库管理难题 你是否厌倦了在多个游戏平台间来回切换Steam、Epic、GOG、EA App、Battle.ne桌面应用游戏开发终极Apache Lucene查询语法完全解析从基础搜索到高级过滤的实用指南终极Apache Lucene查询语法完全解析从基础搜索到高级过滤的实用指南 Apache Lucene是一个功能强大的开源搜索库提供了丰富的查询语法让用上一篇Expo 多智能体代码评审的协调层设计coordinator.md 如何整合专家评审并做出最终决策下一篇如何快速匹配GitHub public roadmap中的产品SKU与行业解决方案完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑