索引结构
ES底层的lucene引擎的分段,比较类似LSM tree的机制。 分段不可变,合并生成新的大的分段。 ES中的translog对应Hbase中的WAL日志,防止进程崩了丢数据;
常用api 查看分析器对某段文本的结果:
1 curl -XPOST 'localhost:9200/get-together/_analyze?analyzer=myCustomAnalyzer' -d 'share your experience with NoSqlιbig data technologies'
组合内置分词器和过滤器,空格分词、小写+反转:
1 curl -XPOST 'localhost:9200/_analyze?tokenizer=whitespace&filters=lowercase,reverse' -d 'share your experience with NoSql big data technolog es'
查看单文档的所有token信息:(get-together索引下、group类型、文档id为1):
1 curl 'localhost:9200/get-together/group/1/_termvector?pretty=true'
分析器 分析器 = 0到1个字符过滤器 + 1个单个分词器 + 0到n个分词过滤器;
标准分析器(默认) standard analyzer
= 标准分词器 + 标准分词过滤器 + 小写转换分词过滤器 + 停用词分词过滤器 (0字符过滤+1分词器+3分词过滤器)
简单分析器 simple analyzer
: 在非字母处进行分词 + 转小写
空白分析器 whitespace analyzer
: 根据空白分词 + 0分词过滤器
停用词过滤器 stop analyzer
: 根据停用词分词 + 0分词过滤器;
模式分析器 pattern analyzer
: 允许指定一个分词切分模式;
语言分析器 包括汉语;
雪球分析器 snowball analyzer
: 标准分析器 + 雪球词干器;
分词器 标准分词器 主要处理欧洲语言,移除标点;
关键词分词器 整个文本提供给过滤器
字母分词器 基于非字母分词
小写分词器 非字母分词+转换成小写
空白分词器 通过空白来分词
模式分词器 例如可以在出现文本._.的地方分词:
1 2 3 4 5 6 7 8 curl -XPOST 'localhost:9200/pattern' -d '{ "settngs": { ”index” : { ”analysis”: { ”tokenizer": { ”patternl”: { ”type": ”pattern”, ”pattern”:”\\.-\\.”
UAX/URL电子邮件分词器 john.smith@example.com => 标准分词 => john.smith example.com
http://example.com?q=foo => 标准分词 http、example.com、q、foo
如果用UAX/URL电子邮件分词器,则可以保留:john.smith@example.com (type:< EMAIL>)http://example.com?q=bar (type:< URL>)
路径层次分词器 path hierarchy tokenizer
输入: /usr/local/var/log/es/log 分词结果: /usr、/usr/local、 …. /usr/local/var/log/es/log 因此有相同父目录的路径搜索(分词有相同部分),能互相搜到。
分词过滤器 标准分词过滤器: 啥也不做; 小写过滤器、停用词过滤器、长度分词过滤器: 将最短和最长的单词过滤掉(自行设置min\max); 截断分词过滤器: 截断超出长度token; 修建分词过滤器: trim 限制分词数量分词过滤器: 限制最多多少个token被索引,比如设置max=8; reverse分词过滤器: 把token反转,可以用于支持后缀索引; 唯一分词过滤器: 每个单词只保留第一次出现的位置(去重了) ascii折叠分词过滤器: 尽量转ascii 同义词分词过滤器: 转成同义词 ngram过滤器: 略 滑动窗口分词过滤器: 略
提取词干 这个好像只是英文有用。把单词缩减到词根。 administrations -> administr 词干提取器: snowball,porter_stem,kstem 字典提取词干: hunspell分词过滤器+字典
打分相关
TF-IDF: 词频、逆文档频率
Okapi BM25;
随机性分歧: DFR相似度
IB相似度;
LM dirichlet相似度;
LM Jelinek Mercer相似度;
BM25 1 2 3 4 5 6 7 8 9 10 11 12 { "mappings" :{ "get-together" : { "properties" : { "title" :{ "type" :"string" ,"similarity" : "BM25" } } } } }
BM25的3个重要参数: k1: 数值, 词频的重要性; (默认1.2) b: 0~1数值, 篇幅对于得分的影响程度; (默认0.75) discount_overlaps: 多个分词出现在同一位置,是否影响长度的标准化(默认true)
boosting: 加权 可以用来修改文档相关性的程序。 包括:
一般使用查询期boosting(避免重新索引全部文档)
相关性、语义搜索的一些方案 首先所有的词向量模型都是基于分布假说的(distributional hypothesis):拥有相似上下文的词,词义相似。
参考: https://zhuanlan.zhihu.com/p/80737146
word embedding
sentence embedding: 更难训练;
word embedding算法: word2vec:Skip-gram模型训练神经网络以预测句子中单词周围的上下文单词。 GloVe:单词的相似性取决于它们与其他上下文单词出现的频率。该算法训练单词共现计数的简单线性模型。 Fasttext:Facebook的词向量模型,其训练速度比word2vec的训练速度更快,效果又不丢失。
网上现有的预训练模型:基于维基百科语料库.
性能更优的方案:
粗排: ES;
精排: 语义模型计算相似度;
工业界主流: 谷歌的bert模型
中文分词IK相关 https://github.com/medcl/elasticsearch-analysis-ik
索引时优先使用analyzer
配置的分词器,对文档进行分词; // 索引时用ik_max_word,尽量多分几个词出来; 查询时优先使用search_analyzer
配置的分词器,对输入进行分词; // 查询时使用ik_smark, 尽量用最长的token去查询; //
1 2 3 4 5 6 7 8 9 10 11 curl -XPOST http://localhost:9200/index/_mapping -H 'Content-Type:application/json' -d' { "properties": { "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" } } }'
实践遇到的问题 分词查询和关键字查询同时使用 分词查询的时候,切分是ik_max_word,最大只切到单词; 比如“工具”就是最小粒度了,因此如果查询的时候使用”工”则不会查询到结果。
如果是默认的标准分词器,则只会有单个字,不会有单词;
所以如果两个都要支持,可以用两个字段,(存两个字段) 一个字段用 ik_max_word, 一个字段用 standard; 查询的时候也是用bool or 连接,命中一个即可。
可参考的解决方案 用fields多加一个不分词的结果(name.raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "properties" :{ "name" : { "type" : "string" , "analyzer" : "standard" , "fields" : { "raw" : { "index" : "not_analyzed" , "type" : "string" } } } } }
整合网上的近义词库 思路1: 自定义一个分词过滤器;// 可复用程度高 思路2: 写入该字段前,先用网上的近义词库把文本解析成空格分割的token,然后用空白分词器索引;(查询时用分词器) // 灵活,不用跟版本 // 由于可以配置多个分词过滤器,所以可以同时配置空格分词过滤器和同义词分词过滤器
维基百科近义词库: 528MBhttp://licstar.net/archives/tag/wikipedia-extractor
某个领域最好的词向量:http://licstar.net/archives/tag/%e8%af%8d%e5%90%91%e9%87%8f
考虑用boost加入相似度因素;(加权)
词向量资料:http://licstar.net/archives/328
聚集 聚集有几个选项:
桶型聚集: (group by) term: 词条聚集,就是统计文档数量; significant_terms: 显著聚集 range: 范围聚集; histogram: 直方图聚集;(类似范围,但是只需要提供间距即可) 嵌套聚集、反嵌套聚集、子聚集: 根据文档关系聚集; 地理距离聚集;
度量型聚集: (agg) stats: 就是统计min,max,avg,count,sum信息; extended_stats: 就是加上标准差这种更冷门的统计信息; percentile: 分位数(近似,可以用compress参数控制精度和内存消耗) cardinatily: 基数,也就是uv;// 近似的,hyperLogLog++, precision_threshold控制精度
过滤器和后过滤器 过滤器
后过滤器:
两者区别
文档->过滤器->查询->后过滤器->查询结果 文档->过滤器->查询->filter聚集->聚集结果
换句话说就是后过滤器不影响聚集,过滤器则影响聚集结果。 filter聚集则只影响聚集。
有一个例外是使用globel聚集,这样即使符合查询的只有2条文档,聚集也会应用到所有的文档上。(聚集比查询结果的数据源大)
文档间的关系 对象类型 输入:
1 2 3 4 5 6 7 8 9 10 11 { "name" : "name1" ,"events" : [ {"title" : "hadoop" ,"date”: "12月" } ,{"title": "es" ,"date”: "6月" } ] }
这种数据实际索引的时候,会把各个字段分别组成数组:
1 2 events.title: ["hadoop","es"] events.date: ["6月",“12月”]
所以搜的时候如果想搜6月的hadoop, 也可以搜出12月hadoop的文档(name1).
嵌套类型 上面的情况可以用嵌套类型解决。 这个时候的索引:
1 2 3 4 5 6 7 8 [events.title: hadoop events.date: 12 月 , events.title: es events.date: 6 月 ]
父子关系和反规范化 父子关系的存储: 1.规范化:父文档和子文档分开存储,然后再存储一个映射关系;// 相关查询: has_parent/has_child 2.反规范化:子文档中存储父文档;(空间换时间)
嵌套json的存储 由于ES的底层Lucence只支持扁平结构,ES支持嵌套json的方法是通过强行打平,如:
1 2 3 4 5 6 7 { "title" : "titl1" ,"location" :{ "name" : "name1" ,"geolocation" : "51.52,-0.09" } }
实际实施到Lucence层的时候是这样存的:
1 2 3 titile: "title1" location.name: "name1" location.geolocation: "51.52,-0.09"
因此我们设计的key一定不要有小数点符号。 而且最好是一对一关系(不是数组)。
父子关系的索引选项: include_in_parent/include_in_root
一对一 嵌套json/对象
一对多 嵌套文档: 索引阶段进行join; // 同分片存储,保证本地连接 父子关系: 查询阶段进行join; // 不同分片,远程连接
多对多 反规法化: 可以处理多对多关系
ES扩展 ES集群使用master-slaver架构,master和slaver用心跳信息来判断彼此的存活; (有点类似hadoop,不知道是不是也有hadoop的HA;hadoop在120个节点的时候namenode容易OOM,不知道ES有没有类似问题) master\slaver互相ping应该会消耗一些带宽,可以考虑调节心跳频率调节性能。
节点下线:先停用(停止数据写入、迁移)
集群升级 重启 直接关闭整个集群,不可用。 然后升级所有节点,重启集群。 有一段时间不可用。
轮流重启 不牺牲可用性的情况下,重启集群; 基本步骤是:关一个节点,升级一个节点,重启这个节点,重新加入集群。 这里有一个关键就是,关闭某个节点的期间,不需要集群自己做rebalance. 因此配置:cluster.routing.allocation.enable
=none
可以用curl发命令修改这个配置。 过后重新设置为all
。
如果副本数>1,上述操作期间服务依然可用。
别名API 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 curl -XPOST 'localhost:9200/_aliases' -d ' { "actions" : [ { "add" : { "index" : "get-together" , "alias" : "gt-alias" } }, { "remove" : { "index" : "old-get-together" , "alias" : "gt-alias" } } ] }'
可以分拆成两个命令:
1 2 curl -XPUT 'http://localhost:9200/get-together/_alias/gt-alias' curl -XDELETE 'http://localhost:9200/old-get-together/_alias/gt-alias'
一个别名可以指向多个索引,甚至指向logs-开头的索引。 (类似于一个逻辑名称) 别名还可以附带一个过滤器。
路由 默认路由策略:文档id 可以手动指定routing=xxx来影响分片行为。
因此可以根据业务,把一起访问的文档路由到同一分片上。
debug api: 查看搜索的分片 routing为xxx时,会搜索哪个分片:
1 curl -XGET 'localhost:9200/get-together/_search_shards&routing=xxxx'
可以在别名中配置路由,简化查询操作。
性能优化 合并请求(bulk接口) 批量新增、批量更新、批量搜索
IO配置优化 segments: 分段;
ES接收到文档后: 分段的倒排索引
概念 refresh: 刷新; 生效,重新打开索引;新建的索引生效, 以前的缓存失效; flush: 冲刷; 刷盘,索引数据写入磁盘; 合并: 小分段合并成大分段; // 分段越多,查询越慢; 存储限流: 调节每秒写入的字节数;
优化策略 刷新频率降低 缓存失效的频率降低,性能更高,新文档慢一些生效; // 默认每秒刷新, index.refresh_interval
刷盘频率降低 IO消耗降低,性能更高,丢数据概率提高; 触发刷盘的时机: 1.内存缓存区已满; 2.固定间隔(定时器); 3.事务日志达到阈值; 因此调控的手段: 1.内存缓存区大小: indices.memory.index_buffer_size; 2.刷新间隔: index.translog.flush_threshold_period; 3.事务日志大小: index.translog.flush_threshold_size;
合并策略优化 合并的作用:
真正删除文档;
分段越少,查询越快;
触发合并的时机:
索引文档;
更新、删除文档;
合并相关配置:index.merge.policy.segments_per_tier
: 每层的分段数量; 高=>写性能越好,越低=>读性能越好;index.merge.policy.max_merge_at_once
: 每次合并多少分段; 设置为等于segments_per_tier
即可;index.merge.policy.max_merged_segment
: 分段的最大规模; 低=>写性能好; 高=>读性能好;index.merge.scheduler.max_thread_count
: 合并用的最大线程数;
存储限流 indices.store.throttle.max_bytes_per_sec
: 最大IO吞吐量,默认20MB/s,默认只针对merge(合并分段); (ssd的话,可以调大到100~200MB)
indices.store.throttle.type
: 限流类型;none
: 不限流, all
: 所有磁盘操作; 默认: merge
。
磁盘IO优化 MMAPDirectory: 进程请求OS对磁盘文件进行内存映射(初始化开销); 也就是mmap,0拷贝,进程挂掉的话,内核会帮忙保存文件;
NIOFDirectory: 进程将磁盘文件复制到JVM堆中; 也就是常规文件访问,进程挂掉,则文件修改丢失;
相关配置:index.store.type
: 默认default
. mmapfs: 只使用MMapDirectory, 静态索引,物理内存能放下索引时适用; niofs: 只使用NIOFSDirectory,32位系统适用;
可以对单个索引配置,也可以配置成全局。
缓存优化 ES的缓存分为:
分片查询缓存: 缓存查询结果;
OS缓存: 缓存索引到内存;
过滤器缓存 过滤器缓存可以在query时,在filter用_cache
:true/false配置。 过滤器缓存在各个节点上,内存占比配置:indices.cache.filter.size
: 默认10% 缓存淘汰策略: LRU 缓存生存时间:index.cache.filter.expire
: 30m (表示30分钟过期)
比较简单的过滤器可以使用bitset来减少内存消耗; 比较复杂的过滤器则直接存储查询结果。 可以使用bitset的过滤器: term,exists/missing,prefix
字段过滤器 索引:token -> 文档 ; (又叫倒排索引) 字段: 文档 -> 词条; (主要用于排序和聚集)
字段上可以用的过滤器: terms过滤器 range过滤器
预热器优化 可以在索引上定义预热器。 即将按日期倒序查询:
1 2 3 4 5 6 7 8 curl -XPUT 'localhost:9200/get-together/event/_warmer/upcoming_events' -d '{ "sort":[ { "date": {"order":"desc"} } ] }'
即将查询热门分组:
1 2 3 4 5 6 7 8 9 curl -XPUT 'localhost:9200/get-together/group/_warmer/top_tags' -d '{ "aggs":{ "top_tags":{ "terms":{ "field": "tags.verbatim" } } } }'
可以创建索引的时候直接定义预热器。
脚本优化 如果只有数值型的操作,可以考虑用lucene表达式代替脚本; 性能: 不用脚本>lucene表达式(js)>java脚本>其他脚本
ES查询过程 topN查询 比如取10个结果: 1.每个分片取得分前10的; 2.合并所有分片的结果(归并排序);
因此得分是在每个分片上算的(分片内得分)
分页查询 { “from”: 400 ,”size”: 100 } 这种需要400~500的结果,但实际会取前500个,然后扔掉前400个。
如果是顺序翻页需求,可以用scroll类型查询优化,查询的时候传递一个scroll=1m的参数,让ES等一分钟,以准备接收下一次翻页。这种情况下,每次ES都会返回一个scrollId。
集群管理 模版 可以配置某个前缀的索引都应用某个配置模版。 (比如模版里定义别名)
多个模版的合并 多个模版可能匹配到同一个索引,这个时候多个模版的配置会合并。 合并的顺序按照order字段,0的先执行,然后1的覆盖0的,依此类推。