搜索引擎

此处列出的通常是高级搜索引擎使用的底层引擎.

Java编写的开源搜索库.
被Elasticsearch和Apache Solr使用.

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, 在输出时会产生正确的位置信息.

考虑到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_]
  • type 字段类型
    • 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)的性能.

该属性的取值来源于源代码中名为Cardinality的枚举.

  • SingleValue: JSON表示为 single, 顾名思义只能有一个元素.
    内部使用IntFastFieldWriter进行写入, 在存储时还会更新该字段当前的最大值和最小值.
  • MultiValues: JSON表示为 multi, 顾名思义可以有多个元素, 该选项被用于支持数字数组.
    需要的内存和CPU比SingleValue要多.

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 也不例外.

Go编写的全文搜索引擎库, 受Apache Lucene启发.

没有内置中文分词器.

在一项2019年的性能测试中, bleve是最慢的底层引擎.

https://github.com/blugelabs/bluge

Go编写的文本索引库.

由C++编写的提供RESTful API的搜索引擎.
有非常多的commits和很大的版本号, 足以证明这是一个成熟的项目.

已经2年没有积极维护, 一些绑定已经有10年没有实质性维护.

不支持中文.

PISA是一个将索引保存在内存里的搜索引擎, 可建立的索引大小受内存限制.

全文搜索前端, 当前后端为PostgreSQL.

为Django/Python设计的全文搜索前端, 支持Solr, Elasticsearch, Whoosh, Xapian作为后端.
https://github.com/django-haystack/django-haystack

商业公司出品, 部分开源.
Java编写, 基于Lucene.

8GB(最低), 16GB, 32GB, 64GB(推荐)

推荐将50%的内存分配给堆内存.
堆内存超过32GB时会遇到性能问题.

2~8核心

单机的性能比主机数量重要.

当需要构建非常复杂的搜索时, Elasticsearch这样的重型通用搜索引擎可以很好的发挥作用(例如日志分析).
与之相对的, 如果只需要一些简单的关键字查询, Elasticsearch很可能是大材小用.

Apache出品, 开源.
Java编写, 基于Lucene.

内存用量取决于具体情况,
但通常来说至少需要分配1GB的堆内存才能让Solr正常运作, 因此需要至少2GB的物理内存.

基于tantivy的搜索引擎.

具有比Bayard高级的功能, 可以用JSON构建查询.
该项目还远未达到完善的程度, 几乎没有文档, 并且在2019年的活跃之后就缺少大的项目进展.

基于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方法调用的.

基于bleve的搜索引擎.

不支持中文, 但支持日文分词器.

基于bluge的搜索引擎.

https://github.com/prabhatsharma/zinc

基于Xapian的RESTful搜索引擎.

该项目缺乏积极维护.

使用外部全文搜索引擎需要从DBMS数据同步, 这会带来很多问题.
内建于DBMS全文搜索引擎可以免去数据同步过程, 直接在数据库里建立索引.
尽管这一优点常常与缺乏中文分词支持的DBMS无缘, 但仍然可以免去同步其他非全文搜索字段的麻烦.

PostgreSQL内置了全文搜索功能, 可以与GIN索引组合实现全文搜索引擎.

https://github.com/zombodb/zombodb

ZomboDB是一项PostgreSQL扩展, 可以让ElasticSearch成为PostgreSQL的表索引.

SQLite内置了全文搜索引擎FTS5.

由Rust编写的即时搜索引擎, 在提供搜索栏功能时, 比Elasticsearch快很多.

MeiliSearch的灵感源于Algolia发布的博客文章, 某种程度上可以视作是Algolia的开源实现.

v0.20及以下版本的引擎是为千万级以下的文档设计的, 和Typesense一样使用Trie数据结构.
据称v0.21开始使用的新引擎可以支持千万级数据.

支持中文分词(通过jieba-rs实现).
项目的文档很全面, 使用起来极其简单.
目前不能分布式部署.

建立索引的过程是异步的, 仅传输数据到MeiliSearch的速度非常快,
但至少在v0.20版本里, 仅支持单线程编制索引, 速度非常慢, 比Sonic还要慢得多.
据称在v0.21的新引擎里, 支持多线程编制索引
(即便如此最终生成的索引大小也仍不可小觑, 所以对大型数据集来说仍然很难称得上有可用性).
异步也导致一些错误的操作不会立即被发现, 例如facted search的字段类型错误.

MeiliSearch不是仅依靠内存工作的,
它使用的存储引擎Lightning Memory-Mapped Database Manager (LMDB)是一个具有ACID特性的内存映射数据库.
MeiliSearch会尽可能利用内存, 内存越大, 则读取硬盘的次数越少, 搜索速度也就越快.
当内存和硬盘上的索引大小相同时, MeiliSearch将达到最高性能.
注: MeiliSearch创建的索引被认为比一般的全文搜索引擎要大.

在v1.0发布之前, 数据库在不同版本之间不兼容.

可以修改查询的相关性排名规则, 手动指定某个字段来影响排序结果.

MeiliSearch支持过滤器, 过滤字段必须是number, boolean, string或这三种数据类型之一的数组.

所有类型都可以使用 =, != 运算符.
number类型支持 >, >=, <, <= 运算符.
在比较string时, 不会区分大小写.

支持 NOT, AND, OR.
逻辑运算是短路的.

MeiliSearch提供的一种缩小搜索结果的方式, 它可以将数据按特定字段的值划分为小的子集,
从而减少搜索的文档数量, 概念上很接近于数据库的分区技术.

分面搜索的字段被定义在索引的设置里, 会导致索引的重建.

从性能的角度考虑, 应该优先使用分面搜索.

为了快速查询, 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会在分词前将文本标准化, 例如将繁体中文转换为"现代中文"(即"简体中文").

作为Redis模块(由C编写)构建的分布式全文搜索和聚合引擎.

索引和查询性能都远高于ElasticSearch.

由Rust编写的标识符索引引擎, 搜索引擎本身并不存储文档.
被视作Elasticsearch轻量级替代品和全文搜索领域的Redis, 可以在几RAM的机器上运行, 速度非常快.

Sonic直接在文件系统上进行搜索(在后端使用RocksDB), SSD对Sonic来说是必须的.

Sonic创建出来的索引显著小于人们对全文搜索引擎的印象, 创建出的索引会比原始数据更小而不是更大.

使用结巴进行分词的PR已经被合并进主分支, 因此在之后的版本里理应能够使用结巴分词的索引.

旧版本也原生支持中文, 但旧版本的中文分词是基于逐字索引实现的.
可能与逐字索引或Sonic的实现原理有关, 并不能真正查询到所有符合条件的中文结果.

作者曾在一个issue中说可以通过 lang: none 选项阻止在push和query时的语言特定行为,
在v1.3.0版本尝试过用此方式进行中文手动分词+逐词push, 获得的结果与不设定 lang: none 时完全一样.

collection 类似于数据库里的database
bucket 类似于数据库里的table
object 类似于数据库里的rowId
terms 类似于搜索关键字keyword

对大部分使用者的应用场景来说, 可能不需要bucket, 在这种情况下可以简单地使用default作为缺省名称.

  • 无法实现相关性排名.
  • 不支持一次查询多个索引.
    因此, 每次只能查询一个关键字(term), 用户需要自行整合查询结果(即手动处理AND, OR).
  • 不支持批量插入数据.
    Sonic只能通过编程方式逐个插入数据, 导入数据可能需要很长时间.
  • 默认情况下的日志等级是用于开发的, 如果不修改或限制很快就会塞满磁盘.

注: 不要将它与Python的文档生成器Sphinx混淆.

据称有非常好的性能表现.

有自己的SQL方言SphinxQL, 模拟了MySQL服务器, 可以用MySQL客户端连接Sphinx.
提供HTTP API和多种语言的Native API.
Sphinx内置了连接到MySQL, MariaDB, PostgreSQL, ODBC提取数据的功能.

遗憾的是, 最新版本Sphinx(Sphinx3, 于2017年末发布)的文档并不完善, 对于很多关键特性没有给出示例.

支持中文.

由JavaScript构建出的小型搜索引擎库, 用于在内存中搜索数据.

自称是速度最快的JavaScript全文搜索库, 在内存占用和搜索速度上都大幅领先于其他库.