搜索引擎

此处列出的通常是高级搜索引擎使用的底层引擎.
Rust编写的全文搜索引擎库, 受Apache Lucene启发, 作者在其博客详述了技术细节.
tantivy比Lucene更快.
文档很差, 有很多地方与代码不一致或干脆没有文档, 阅读源码是必须的.
中文支持是由第三方分词器完成的.
tantivy使用LZ4压缩文档存储.
为英文维基百科的500万篇文章建立索引, 速度很快, 索引在合并后甚至比原始数据还要小一点.
作为使用硬盘存储的搜索引擎, 查询性能很好, 2个关键字的OR查询可以在100ms左右返回相关性排序的结果.
即使在低内存的单核机器上也非常有效率.
tantivy的分词器设计要求在分词时返回文本的位置信息, 但由于jieba在分词时不会提供位置信息,
cang-jie采用了一种先进行分词, 然后再生成位置信息的方案:
https://github.com/DCjanus/cang-jie/blob/b828f21939dae6dcb0a4cdaaa0f147e0e8263af2/src/stream.rs
然而, 对于All/ForSearch这样的选项, 由于会将歧义内容多次分词, 返回的文本是比实际的文本更多的.
将这些选项用于分词时, 返回的位置信息一定是错误的.
与cang-jie不同, 使用了jieba-rs的TokenizeMode, 在输出时会产生正确的位置信息.
由于jieba-rs的API限制, 该分词器会在初始化时就分词文本, 无法像迭代器那样工作.
由于tantivy对 TokenStream 的使用方式, 这种做法目前似乎还没有造成大的问题, 但在未来可能成为隐患.
考虑到tantivy还没有达到v1.0版本, 加上各种API缺失的问题, 自己封装可能还比较方便.
tantivy官方的Python3绑定.
除非修改源代码, 否则无法在tantivy-py使用第三方tokenizer.
同时也缺少配置这些可配置tokenizer的选项, 例如cang_jie的hmm开关和分词方案.
https://github.com/tantivy-search/tantivy-py/issues/25
不支持在搜索时用facet缩小结果范围.
https://github.com/tantivy-search/tantivy-py/pull/21
用neon实现的第三方Node.js绑定.
该项目的包装事实上是非常浅的, 并且不是TypeScript.
作者只给出了最基础的示例, 高级使用方式必须从源码倒推.
面临和tantivy-py一样的第三方tokenzier支持问题.
用neon实现的第三方Node.js绑定, 使用的是napi.
已经两年没有更新.
面临和tantivy-py一样的第三方tokenzier支持问题.
tantivy的Node.js包装, 使用stdio作为RPC接口.
由于采用了这种包装形式, 代码比neon要复杂得多.
面临和tantivy-py一样的第三方tokenzier支持问题.
  1. 1.
    创建索引的schema, tantivy的索引是静态的, 因此必须事先创建好.
  2. 2.
    创建一个具有固定大小内存缓冲区的索引写入器IndexWriter.
  3. 3.
    将文档(JSON)插入到索引编写器的队列.
    当文档占用空间超出缓冲区时, 写入器会阻塞直到它将文档编制成serialized segment.
  4. 4.
    提交更新(commit), 索引写入器将segment正式应用到索引, 变成committed segment.
    commit完成之前, segment被称作uncommitted segment, 无法被搜索到.
    频繁调用commit会加剧segment的碎片化, 进而导致需要更多的时间执行merge.
    执行commit所需的时间与未纳入索引的文档量直接相关..
  5. 5.
    合并(merge)碎片段(fragmented segments):
    tantivy索引由segment组成, 这一步骤即是将索引里碎片化的segment合并.
    执行此操作会减少索引的磁盘用量(尤其是对全新的索引来说, 体积会大幅减小), 并且提高冷索引的查询性能.
    该步骤是可选的, 执行merge需要很长时间, 因此会长时间阻塞IndexWriter, 并且需要临时占用额外的磁盘空间.
文档对于schema的描述寥寥, 建议阅读源代码.
从tantivy 0.17开始, 支持JSON字段, 从而支持无模式索引, 无模式在一些场景里会有一定性能损失.
  • name 字段名称 [a-zA-Z0-9_]
    • str
    • u64
    • i64
    • f64
    • HierarchicalFacet
      分面类型是一种值必须以 / 开头的文本类型数据, 可以会以数组的形式表示多个值, 例如:
      ["/category/search/server", "/language/rust"]
      在查询时可以以前缀的形式在查询字符串中指定需要查询的facet.
    • bytes
    • Date (rfc3339): 在内部会转换为u64类型的UTC时间戳(根据文档, 实际上是i64).
根据Document的文档和源码, 可知tantivy里文档的字段允许有多个值(内存表示为 (field, value)[]),
同名的重复字段JSON里是以数组的形式表达的, 但 只有最后一个值会被索引.
  • stored 决定是否将值持久化存储, 未持久化的字段将不会出现在返回的文档中.
    停止持久化那些仅用于搜索的字段, 可以大幅缩小tantivy的数据库体积.
  • indexing, 此项可为 null, 代表不索引:
    • record:
      • basic 只记录文档id.
      • freq 记录文档id和词频, 词频会被用于排序.
      • position 记录文档id, 词频和出现在文档中的位置, 必须开启此项才能使用短语查询.
    • tokenizer: 分词器 default
      • raw 不分词
      • default 简单分词
      • en_stem
      • PreTokenizedString, 手动分词.
        需要提交需要满足以下结构的token:
struct Token {
offset_from: usize,
offset_to: usize,
position: usize,
text: String,
position_length: usize,
}
  • indexing, boolean值, 决定是否索引.
  • fast:
    等价于Apache Lucene的DocValues.
    这是一种列式索引, 可以优化排序和分面(facet)的性能.
HierachicalFacet的属性是隐含的.
在内部, 尽管HerachicalFacet字段是文本类型, 但它会自动获得值为MultiValues的fast属性.
tantivy使用QueryParser自定的语法进行查询.
order_by_u64_field和order_by_fast_field仅支持降序排序:
https://github.com/tantivy-search/tantivy/issues/906
一种临时的解决方案是通过tweak_score或custom_score自行实现自定义排序.
在编写测试时会发现, 使用TopDocs搜索出来的结果排序不稳定, 尽管文档声称它是稳定的.
深入研究会发现, 排序不稳定的原因实际上和TopDocs无关,
而是因为IndexWriter的多线程写入导致文档的顺序不稳定:
https://github.com/quickwit-oss/tantivy/issues/1284
https://docs.rs/tantivy/0.15.2/tantivy/query/struct.QueryParser.html
tantivy自带的查询解析器, 有自己特定的语法.
parse_query只能按term查找, term的大小写不敏感.
关键字转义通过给关键字左右加上双引号实现.
text:"OR"
单独的 -field:value 是不能像一般意义上的"排除某值"那样正常工作的,
因此会抛出 QueryParserError::NoDefaultFieldDeclared.
如果用 () 包装 negative terms, 可以绕过错误检查, 但也同样不能如期工作.
以下形式都是错误的:
  • -field:value
  • * -field:value
  • field:* -field:value
  • another_field:some_value -field:value
正确的使用方式是 field:value1 -field:value2.
IndexWriter.commit函数返回仅代表索引更新完毕, 并不能代表IndexReader重新加载了索引.
因此会出现调用commit后立即通过IndexReader进行搜索, 却与最新数据不符的情况.
根据文档, IndexReader默认的自动重新加载会在10ms完成, 但实际使用中似乎需要更多时间.
在测试环境下, 应该通过IndexReader.reload函数手动重新加载, 以确保加载了最新的数据.
tantivy支持基于term的删除, 因此提交的文档应该带有某个可以作为唯一标识的字段.
与写入一样, 删除操作只有在提交后才会正式应用到索引, 即使对 IndexWriter.delete_all_documents 也不例外.
Java编写的开源搜索库.
被Elasticsearch和Apache Solr使用.
Go编写的全文搜索引擎库, 受Apache Lucene启发.
没有内置中文分词器.
在一项2019年的性能测试中, bleve是最慢的底层引擎.
https://github.com/blugelabs/bluge
Go编写的文本索引库.
https://github.com/Kronuz/Xapiand
由C++编写的提供RESTful API的搜索引擎.
有非常多的commits和很大的版本号, 足以证明这是一个成熟的项目.
已经2年没有积极维护, 一些绑定已经有10年没有实质性维护.
不支持中文.
PISA是一个将索引保存在内存里的搜索引擎, 可建立的索引大小受内存限制.
商业公司出品, 部分开源.
Java编写, 基于Lucene.
8GB(最低), 16GB, 32GB, 64GB(推荐)
推荐将50%的内存分配给堆内存.
堆内存超过32GB时会遇到性能问题.
2~8核心
单机的性能比主机数量重要.
当需要构建非常复杂的搜索时, Elasticsearch这样的重型通用搜索引擎可以很好的发挥作用(例如日志分析).
与之相对的, 如果只需要一些简单的关键字查询, Elasticsearch很可能是大材小用.
Apache出品, 开源.
Java编写, 基于Lucene.
内存用量取决于具体情况,
但通常来说至少需要分配1GB的堆内存才能让Solr正常运作, 因此需要至少2GB的物理内存.
不支持自定义分词器: https://github.com/lnx-search/lnx/issues/94
具有比Bayard高级的功能, 可以用JSON构建查询.
该项目还远未达到完善的程度, 几乎没有文档, 并且在2019年的活跃之后就缺少大的项目进展.
https://github.com/mosuka/bayard
基于tantivy实现的搜索引擎前端, 具有CLI, gRPC和REST服务器等多种访问方式.
和tantivy距离很近, 但比tantivy的文档全面.
内置了n-gram分词器.
支持中文分词, 由cang-jie实现(jieba-rs的tantivy绑定).
Bayard的查询字符串是由tantivy的QueryParser解析的, 因此查询语法也直接来源于tantivy, 文档中并没有说明这一点.
Bayard直接通过JSON定义tantivy的schema, 但没有做任何抽象层:
Schema结构体是直接通过serde_json读取JSON得来的, 因为Schema实现了serde的Deserialize特型.
如果需要使用多个索引, 则必须开多个服务器.
任何形式的错误都会导致bayard服务器停止响应.
可以认为该项目目前还非常不成熟, 没有使用价值.
例如commit和merge是通过GET方法调用的.
https://github.com/prabhatsharma/zinc
基于bluge的搜索引擎.
全文搜索前端, 当前后端为PostgreSQL.
为Django/Python设计的全文搜索前端, 支持Solr, Elasticsearch, Whoosh, Xapian作为后端.
https://github.com/django-haystack/django-haystack
使用外部全文搜索引擎需要从DBMS数据同步, 这会带来很多问题.
内建于DBMS全文搜索引擎可以免去数据同步过程, 直接在数据库里建立索引.
尽管这一优点常常与缺乏中文分词支持的DBMS无缘, 但仍然可以免去同步其他非全文搜索字段的麻烦.
PostgreSQL内置了全文搜索功能, 可以与GIN索引组合实现全文搜索引擎.
https://github.com/zombodb/zombodb
ZomboDB是一项PostgreSQL扩展, 可以让ElasticSearch成为PostgreSQL的表索引.
专用于搜索和分析的PostgreSQL解决方案.
其中的pg_bm25(将被重命名为pg_search)是PostgreSQL的tantivy扩展, 可以为PostgreSQL提供基于tantivy的全文搜索索引.
该项目的中文分词器是从Quickwit复制而来的:
https://github.com/paradedb/paradedb/blob/f508b55a80343b2500dae7a1888c7631106d7656/tokenizers/src/cjk.rs
SQLite内置了全文搜索引擎FTS5.
由Rust编写的即时搜索引擎, 在提供搜索栏功能时, 比Elasticsearch快很多.
MeiliSearch的灵感源于Algolia发布的博客文章, 某种程度上可以视作是Algolia的开源实现.
v0.20及以下版本的引擎是为千万级以下的文档设计的, 和Typesense一样使用Trie数据结构.
据称v0.21开始使用的新引擎可以支持千万级数据.
项目的文档很全面, 使用起来极其简单.
目前不能分布式部署.
MeiliSearch不是仅依靠内存工作的,
它使用的存储引擎Lightning Memory-Mapped Database Manager (LMDB)是一个具有ACID特性的内存映射数据库.
MeiliSearch会尽可能利用内存, 内存越大, 则读取硬盘的次数越少, 搜索速度也就越快.
当内存和硬盘上的索引大小相同时, MeiliSearch将达到最高性能.
注: MeiliSearch创建的索引被认为比一般的全文搜索引擎要大.
可以修改查询的相关性排名规则, 手动指定某个字段来影响排序结果.
MeiliSearch支持过滤器, 过滤字段必须是number, boolean, string或这三种数据类型之一的数组.
所有类型都可以使用 =, != 运算符.
number类型支持 >, >=, <, <= 运算符.
在比较string时, 不会区分大小写.
支持 NOT, AND, OR.
逻辑运算是短路的.
MeiliSearch提供的一种缩小搜索结果的方式, 它可以将数据按特定字段的值划分为小的子集,
从而减少搜索的文档数量, 概念上很接近于数据库的分区技术.
分面搜索的字段被定义在索引的设置里, 会导致索引的重建.
从性能的角度考虑, 应该优先使用分面搜索.
建立索引的过程是异步的, 仅传输数据到MeiliSearch的速度非常快.
但异步也导致一些错误的操作不会立即被发现, 例如facted search的字段类型错误.
在v0.20版本里, 仅支持单线程编制索引, 速度非常慢, 比Sonic还要慢得多.
据称在v0.21的新引擎里, 支持多线程编制索引.
支持中文分词(通过jieba-rs实现), 但在v1.0.0版本中开始将汉字标准化为拼音, 导致相关问题:
Chinese Pinyin search is not accurate in few Chinese character
为了快速查询, MeiliSearch在搜索时只会考虑查询字符串的 前10个词.
文档中的单个字符串字段最多只包含1000个词, 超过1000个词时, 剩余的内容不会被索引.
MeiliSearch建立的索引体积非常大, 在常见的情况下, 会产生同等JSON文件10倍左右大小的索引,
尽管实际使用的物理内存可能远小于索引大小, 但如此巨大的索引体积仍然是不可接受的.
此外, MeiliSearch也没有回收掉已经删除的空间的能力, 被删除的空间只能被用于后来插入的新文档.
由C++编写的轻量级开源搜索引擎.
文档比较简陋.
在功能定位上与MeiliSearch非常相似.
Typesense将索引 完全载入到内存, 将文档保存在RocksDB里.
出于Typesense的升级性考虑, 启动时需要从文档里重新索引, 因此在大数据集上需要很长的启动时间.
可分布式部署.
即时搜索引擎服务商, 提供搜索即服务(SaaS).
客户将数据通过RESTful API推送到Algolia, 然后将搜索框添加到网页上.
https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/normalization/
Algolia会在分词前将文本标准化, 例如将繁体中文转换为"现代中文"(即"简体中文").
由Rust编写的标识符索引引擎, 搜索引擎本身并不存储文档.
被视作Elasticsearch轻量级替代品和全文搜索领域的Redis, 可以在内存极小的机器上运行, 速度非常快.
Sonic直接在文件系统上进行搜索(在后端使用RocksDB), SSD对Sonic来说是必须的.
自v1.3.2引入结巴分词, 从而支持中文.
Sonic创建出来的索引显著小于人们对全文搜索引擎的印象, 创建出的索引会比原始数据更小而不是更大.
collection 类似于数据库里的database
bucket 类似于数据库里的table
object 类似于数据库里的rowId
terms 类似于搜索关键字keyword
对大部分使用者的应用场景来说, 可能不需要bucket, 在这种情况下可以简单地使用default作为缺省名称.
  • 无法实现相关性排名.
  • 不支持一次查询多个索引.
    因此, 每次只能查询一个关键字(term), 用户需要自行整合查询结果(即手动处理AND, OR).
  • 不支持批量插入数据.
    Sonic只能通过编程方式逐个插入数据, 导入数据可能需要很长时间.
  • 默认情况下的日志等级是用于开发的, 如果不修改或限制很快就会塞满磁盘.
作为Redis模块(由C编写)构建的分布式全文搜索和聚合引擎.
索引和查询性能都远高于ElasticSearch.
注: 不要将它与Python的文档生成器Sphinx混淆.
据称有非常好的性能表现.
有自己的SQL方言SphinxQL, 模拟了MySQL服务器, 可以用MySQL客户端连接Sphinx.
提供HTTP API和多种语言的Native API.
Sphinx内置了连接到MySQL, MariaDB, PostgreSQL, ODBC提取数据的功能.
遗憾的是, 最新版本Sphinx(Sphinx3, 于2017年末发布)的文档并不完善, 对于很多关键特性没有给出示例.
由JavaScript构建出的小型搜索引擎库, 用于在内存中搜索数据.
自称是速度最快的JavaScript全文搜索库, 在内存占用和搜索速度上都大幅领先于其他库.
由tantivy的官方团队开发的开源云原生搜索引擎, 专用于 日志等不可变的仅追加内容 的全文搜索.
提供Elasticsearch和OpenObserve的API兼容性.
Quickwit的云原生体现在它解耦了计算和存储, 因此:
  • 计算是无状态的, 因此可以轻松添加和减少搜索实例.
  • 索引可以建立在Amazon S3这样的高延迟低速度存储服务上, 从而极大地降低存储成本.
当前使用的是在#2008中添加的逐字符分词器:
https://github.com/quickwit-oss/quickwit/blob/c7bffd60df884a00fd70ad190861fa4a9cd028c4/quickwit/quickwit-query/src/tokenizers/chinese_compatible.rs