写点什么

Java 技术栈【搜索引擎】:ES 基础

作者:L L
  • 2024-03-11
    广东
  • 本文字数:6036 字

    阅读完需:约 20 分钟

Java 技术栈【搜索引擎】:ES 基础

01 Apache Lucene


对于数据搜索,一般分为 2 大类:

  1. 结构化数据:有固定的数据模型和特定的数据类型,常存储于关系型数据库中,利用索引实现快速搜索。

  2. 非结构化数据:包括文本、图片、音视频等。可以通过顺序扫描、全文检索等方式实现搜索。


全文检索的基本思路是提取非结构化的部分信息,重新组织成有一定结构作为索引(比如倒排索引)实现高效搜索效率。


Apache Lucene 是一个非常高效的全文检索引擎框架,是个工具包,不是一个完整的全文检索引擎。


目前大部分开源搜索引擎都是基于 Lucene 进行二次开发。比如 Solr 和 ElasticSearch(简称 ES) 就是基于 Lucene 建立的成熟的全文搜索引擎。

  1. ES 采用分布式架构,具有分布式索引和搜索的能力,易安装使用。

  2. ES 相对注重核心功能;Solr 则提供更多成熟、稳定的功能,这样配置和使用会更复杂。Solr 后来的版本才加入分布式支持(需要借助 ZK 实现)。


在实际使用时,了解更多 ES 和 Solr 的特点、适用场景,再选择最适合的搜索引擎。


相关技术

倒排索引

倒排索引:从词到文档的索引,适合做关键字检索,可以比较好控制数据的总量,提高查询效率。 中文词只有几十万个

  • Term:词条,索引里最小的存储和查询单元;英文(一个单词),中文(一个词)

  • Term Dictionary:词典,Term 的集合

  • Term Index:存储 Term 前缀和 Term 前缀在 Term dictionary 的偏移量的索引

  • Posting list:映射表/倒排表,记录某个词在哪些文档出现以及出现的位置(偏移)、权重;存储量可能很大,需要考虑压缩等

  • Inverted File:倒排文件,存储倒排索引的物理文件

  • 分段存储,一个索引文件,拆分为多个子文件,每个子文件是段,修改的数据不影响的段不必做处理;需要定期对段进行合并


ES 的倒排索引:包括词典 Term(还有排好序的 Term Dictionary、Term Index)和倒排表 Posting list。

  1. 当写入一个文档时,根据文档每个字段,使用合适分词策略,把每个字段切割成一个个关键词 Term

  2. 然后统计每个 Term 出现的频率,构建 Term 到文档 ID、出现频率、位置(第几个词,起始位置)的映射,即 Posting list


#动态索引:基于模板 + 时间 + rollover api 滚动创建索引


TF-IDF

TF-IDF 用来进行文本相似度计算,可以用来做权重指标。

  1. TF 词频,一个词在这个文档中出现的频率,正向指标。

  2. IDF 反向文档频率,一个词在所有文档中出现的频率,反向指标。


FST

对于关键词查找,业界成熟的方案是字典树,ES 使用了 FST 数据结构,做了进一步优化。Lucene 从 4+ 版本后也大量使用 FST。

FST(Finite State Transducer)优点:

  1. 空间占用小,对词典中单词前缀和后缀一并压缩

  2. 查询速度快,划分多个 Block 加快查找速度。Block 还可以进一步细分为 Floor Block


02 ES

ES 是一个基于 Lucene 框架的开源搜索引擎产品,提供 RESTful 风格的 API,是一个分布式、可扩展、近实时的全文搜索与数据分析引擎,使用 Java 编写。

  1. 提供强大的分布式文件存储能力、分布式实时分析搜索能力、实时全文搜索能力。

  2. 强大的集群扩展能力(能胜任上百个服务节点的扩展),PB 级别的结构化和非结构化数据处理能力。

使用场景

  1. 全文搜索

  2. 数据异构,如引入 Canal 数据同步工具,订阅 MySQL 的 binlog,将增量数据同步到 ES 中,实现数据访问层的读写分离;或做宽表,解决跨库 join

  3. ES 也有很强大的计算能力,可以用在大数据量查询、计算的场景,比如实现用户画像功能

基本概念

  • Index: 类似关系数据库中的 table

  • Mapping: schema

  • Document: row,简单理解为 field 的集合

  • Field: column

// 索引结构PUT my_index {    "mappings": {      "_doc": {         "properties": {           "name":     { "type": "text"  },           "age":      { "type": "integer" },            "created":  {            "type":   "date",             "format": "strict_date_optional_time||epoch_millis"          }        }      }    }  }
复制代码


Index 索引

  1. ES 为了能快速找到某个 Term,先将所有的 Term 排序,然后根据二分法查找 Term,时间复杂度为 log(N),就像通过字典查找一样,这就是 Term Dictionary。

  2. 如果 Term 太多,Term Dictionary 会很大,不适合全部放在内存里,于是有了 Term Index,像字典里的索引页一样。

  3. Term Index 包含的是 Term 的一些前缀。通过 Term Index 可以快速地定位到 Term Dictionary 的某个偏移量,然后从这个位置再往后顺序查找。

  4. 在内存中用 FST 方式压缩 Term Index,以字节的方式存储所有的 Term,可以有效的缩减存储空间,使得 Term Index 足以放进内存(但压缩会导致查找时需要更多的 CPU 资源)

  5. 存储在磁盘上的倒排表,也采用了压缩技术减少存储所占用的空间。


索引中每个文档可以有不同的结构,索引包含一个 Mapping,Mapping 可以定义多种字段类型。


Mapping 映射

ES 对索引中的文档(Document )和 字段(Field)如何存储和被索引,是通过 Mapping 定义的。


Mapping 就像数据库中的 Schema ,描述了文档可能具有的字段或属性、每个字段的数据类型,分词方式、是否存储等。

有 2 种 Mapping 类型:

  1. 动态映射(Dynamic Mapping):创建索引时不指定具体字段的类型,ES 根据数据格式自动识别字段类型。

  2. 显式映射(Explicit Mapping):创建索引时指定具体字段类型,也被称为静态映射。


字段类型(Field Data Type):

  1. 核心类型:

① text:用于索引全文值的字段,例如邮件正文或产品说明,不用于排序,很少用于聚合;这些字段是被分词的,通过分词器传递

② keywords:用于索引结构化内容的字段;例如邮件地址、主机名、状态代码等,通常用于过滤、排序和聚合;只能按其确切值进行搜索

③ long, integer, short, double, data, boolean 等

  1. 复杂类型:object, nested

  2. 地理类型:geo_point, geo_shape

  3. 特殊类型:ip, completion, token_count, join 等

Text Analysis 文本分析

ES 实现全文搜索功能,主要依赖 Analysis。

Analysis 主要包括 2 个过程:

  1. Tokenization:分词,搜索进行匹配,这个阶段搜索只能匹配一条记录

  2. Normalization:把相似词处理成标准格式,这样搜索结果才能包含多条记录


Analysis 由 Analyzer(分析器)执行,Analyzer 是一组控制整个搜索过程的规则。


建立索引和进行全文搜索请求时,都需要用到 Analysis,确保两个阶段使用相同的 Analyzer,不然搜索可能达不到预期效果。

// 层级关系如下:PUT /example_index {    "settings": {        "analysis": {            "analyzer": {                "analyzer_name": {                  "type": "standard",                    "tokenizer": "standard",                    "char_filter": [ "html_strip" ],            "filter": [ "lowercase" ]                 }             }         }     } }
复制代码

Analyzer 分析器

Analyzer 由一个 Tokenizer 和 0 或多个 Filter 组成。

  1. Character filters:预处理文本,如过滤 HTML 标签

  2. Tokenizer:分词器

  3. Token filters:将切分的单词进行加工,如词干提取(Stemming),大小写转换,去除停用词,加入同义词等


 内置分析器类型:

  • standard:默认分析器,划分文本是通过词语来界定的,由 Unicode 文本分割算法定义。它删除大多数标点符号,将词语转换为小写(就是按照空格进行分词);适用于大多数语言

  • simple:分析器每当遇到不是字母的字符时,将文本分割为词语。它将所有词语转换为小写

  • whitespace:基于空格字符切词

  • stop:是 simple analyzer 的基础上,移除停用词

  • keyword:不切词,将输入的整个串一起返回

  • pattern

  • language

  • fingerprint


支持自定义分析器


// 使用 simple 内置分析器POST _analyze {  "analyzer": "simple",  "text":     "The quick brown fox."}// 自定义分析器(先关闭索引,更新完再打开)PUT my-index-000001 {  "settings": {    "analysis": {      "analyzer": {        "std_folded": {           "type": "custom",          "tokenizer": "standard",          "filter": ["lowercase", "asciifolding"]        }      }    }  },  "mappings": {    "properties": {      "my_text": {        "type": "text",        "analyzer": "std_folded"       }    }  }}
复制代码


Tokenizer 分词器

分词,需要重点考虑效率和准确率的平衡。


内置分词器类型:

  • standard:默认的分词器根据 Unicode 文本分割算法,以单词边界分割文本。它删除大多数标点符号。它是大多数语言的最佳选择

  • letter:遇到非字母时分割文本

  • lowercase:类似 letter ,遇到非字母时分割文本,同时会将所有分割后的词元转为小写

  • whitespace:遇到空白字符时分割位文本

  • uax_url_email:类似 standard,但是可以识别 url 和 email 地址

  • classic:基于英文语法

  • thai:基于泰文

// 使用默认分词器POST _analyze {  "tokenizer": "standard",  "text": "this is standard tokenizer!!!!."}// 更新分词器PUT my_index/_settings {  "analysis" : {    "analyzer" :{       "content" : {          "type" : "custom" ,          "tokenizer" : "whitespace"       }    }  }}
复制代码


中文分词器,IK

ES 默认没有提供中文分词器,可以使用 IK 分词器:下载 IK 分词器包,解压后放到 ES 目录下的 plugins 目录下,重启 ES,即可使用。


对比学习 HanLP 中文分词功能:

https://github.com/hankcs/HanLP/blob/1.x/README.md


IK 分词器 有 2 种拆分方法:

  1. ik_max_word:将文本做最细粒度的拆分,尽可能多的拆分词语

  2. ik_small:做最粗粒度的拆分,已被分出的词语将不会再次被其它词语占有

curl -XPUT 'http://xxxx:port/iktest?pretty' -d '{  "settings" : {    "analysis" : {      "analyzer" : {        "ik" : {          "tokenizer" : "ik_max_word"        }      }    }  },  "mappings" : {    "article" : {      "dynamic" : true,        "properties" : {          "subject" : {            "type" : "string",            "analyzer" : "ik_max_word"          }        }      }   }}'
复制代码


可以通过修改 IK 配置文件支持更多功能,如添加网络热词、词库热更新等。

/plugins/analysis-ik/config/IKAnalyzer.cfg.xml


词库热更新方案:

① 用 Tomcat 作为远程词库的容器:在 /webapp/ROOT 这个路径下新建一个远程词库 hot.dic,并添加实时热词

② 更改 IKAnalyzer.cfg.xml:把 remote_ext_dict 的值设置为 Tomcat 远程词库的访问地址


Normalizer 规范化器

ES 内置小写 Normalizer;可以自定义配置 Normalizer 支持更多场景。

Normalizer 组成和 Analyzer 类似,由 Character filters 和 Token filters 组成。

Normalizer 是对词进行处理,不需要 Tokenizer。

PUT index {  "settings": {    "analysis": {      ...,      "normalizer": {        "my_normalizer": {          "type": "custom",          "char_filter": ["quote"],          "filter": ["lowercase", "asciifolding"]        }      }    }  },  "mappings": {    "properties": {      "foo": {        "type": "keyword",        "normalizer": "my_normalizer"      }    }  }}
复制代码


分布式架构

ES 采用分布式架构,实现了集群的管理功能。

相关概念:

  • cluster:ES 集群,由一个或多个 ES  节点 组成

  • node:集群中节点,节点配置 cluster.name 即可加入对应集群

  • sharding:分片,索引会被划分为一个或多个主分片 (写操作只能在主分片的节点进行)

  • replicas:副本,每个主分片设置一个或多个副本,主分片和副本分片一般跨节点存储,防止机器和网络故障

// demoPUT my_index {     "settings" : {        "number_of_shards" : 5,        "number_of_replicas" : 1     }    "mappings": {      "_doc": {         "properties": {           "title":    { "type": "text"  },           "name":     { "type": "text"  },           "age":      { "type": "integer" },            "created":  {            "type":   "date",             "format": "strict_date_optional_time||epoch_millis"          }        }      }    }  }
复制代码


ES 集群状态通过 绿,黄,红来标识:

  1. 绿色:集群健康完好,所有主分片和分片副本功能正常。

  2. 黄色:预警状态,所有主分片功能正常,但至少有一个副本分片异常。集群可以正常工作,但是高可用性在某种程度上会受影响。

  3. 红色:一个或多个主分片及其副本异常不可用。对于分配到异常分片的写入请求将会报错,最终会导致数据的丢失。集群会继续从可用的分片提供搜索请求服务,但是返回的结果不一定准确。需要尽快修复那些未分配的分片。

主要流程

写入数据

  1. 写请求可以被发送到任意一个节点(该节点成为协调节点)

  2. 协调节点根据路由公式计算出需要写哪个分片,再把请求转发到该分片的主分片节点上(写请求只能写到主分片上)

hash( { routing } ) % number_of_primary_shards

{ routing } 默认是文档的 _id,也可以自定义

  1. 主节点并发将数据复制到其他副本分片上(通过乐观锁并发控制数据的冲突,如 _version)

  2. 所有副本分片响应成功,主分片节点向协调节点响应成功,协调节点向客户端响应成


以上流程是在 ES 的内存中执行的,数据被分配到特定分片和副本上后,最终要存储在磁盘上


查询数据

  1. 使用 RESTful API 向集群发送查询请求

  2. 集群随机选择一个 node 处理这次请求,根据文档 id 计算数据在哪个分片上,返回 主分片和副本分片的节点集合。 这样会负载均衡地把查询发送到对应节点

  3. 如果无法确认 sharding,如检索的不是一个文档,则遍历所有分片;协调节点将查询请求广播到每一个数据节点,这些数据节点的分片就会处理该查询请求

  4. 之后对应节点接收到请求,将文档数据(文档 id、节点信息、分片信息)返回协调节点

  5. 协调节点将所有的结果进行汇总,并排序,把数据返回给客户端。


使用方式

Java 应用程序和 ES 有 2 种交互模式:

  1. Node Client 模式: 应用程序了解整个集群的状态,可以接收集群请求,然后直接转发到目标 ES 节点进行处理,可以减少一定网络访问。

  2. Transport Client 模式:应用程序利用 transport 模块远程连接一个 ES 集群,并不加入到集群中,只是简单获得一个或者多个 transport 地址,并以轮询的方式将所有请求都转发到 ES 节点处理。


如果要创建很多的连接,使用 Node Client 的话,会有很多个 Node Client  加入集群,给集群造成很大负担。


ES 提供 RESTful 风格的 API,使用操作很方便。

例如 Java 版本客户端的官方文档:

https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/getting-started-java.html


下期预告:Java 技术栈【搜索引擎】:ES 集群和生态



发布于: 刚刚阅读数: 3
用户头像

L L

关注

未来不足惧过往不须泣,只有时间才最懂人心 2023-09-12 加入

Java技术栈/办公效能、学习思维、生活旅行分享。致力成为“写作中攻城狮高手,攻城狮中写作高手”。 欢迎关注公众号:过去那点事儿

评论

发布
暂无评论
Java 技术栈【搜索引擎】:ES 基础_ES搜索_L L_InfoQ写作社区