正排索引
文档id到文档内容、单词的关联关系
倒排索引
单词到文档id的关联关系
单词词典
记录所有文档的单词,一般比较大
记录单词到倒排列表的关联信息
一般为B+Tree
倒排列表
记录了单词对应的文档集合,由倒排索引项组成
倒排索引项主要组成部分:
1.文档id
用于获取原始信息
2.单词频率
记录该单词在文档出现的次数,用于后续相关性算分
3.位置:
记录单词在文档的分词位置,用于词语搜索,高亮显示
4.偏移:
记录单词在文档的开始和结束的位置
大概的查询过程
用户输入关键词通过倒排索引先从单词字典中的b+tree结构定位到某个子节点,该子节点对应到倒排列表中的某个位置,通过倒排列表的对应位置的信息获取对应的文档id,然后根据文档id查询到完整的内容并返回结果
在es中,每个字段都会有自己的倒排索引
分词:
分词是指将文本转换成一系列的单词(term or token)的过程,也可以叫文本分析,在es里面称为Analysis,为什么要使用分词呢?可以简单的理解为提高搜索的准确度,分词器越合适,形成的倒排索引在查询时得到的结果也越准确
分词器是es中专门处理分词的组件,组成部分如下
Character Filter:针对原始文本进行处理,去除html标记符
Tokenizer:将原始文本按一定的规则切分为单词
Token Filters:针对tokenizer处理的单词进行再加工,比如转小写,删除或新增某些字符
es中的测试分词api接口为_analyze
1.可以直接指定analyzer进行测试
2.可以直接指定索引中的字段进行测试
3.可以自定义分词器进行测试
下面先用自带的标准分词器进行分词测试
token为分词结果,offset为描述位置的信息
elasticsearch内置分词器,通过analyzer指定
默认分词器 ,小写处理,去掉stop word(要配置),按词切分,支持多语言,
按照非字母切分,小写处理
按空格切分
比simple多了stopword处理,去除一些不必要的字段
不分词,直接将输入作为一个单词输出
通过正则表达自定义分隔符,默认是\W+,非字词符号作为分隔符
提供多种常见语言分词器
另外:
中文分词
下载zip包,解压到plugin的ik目录下,重启es
Character Filter
在Tokenizer之前对原始文本进行处理
htmlscript去除html标签和转换html实体
mapping进行字符替换操作
pattern relace进行正则匹配
会影响后续tokenizer解析的postion和offset信息
Tokenizers
根据某种算法切分字符串,max_token_length限定切分后的每个字符长度,默认255
遇到非字母字符时进行切分
letter tokenizer+lowercase token filters
根据空白格切分,也有最长字符限定配置
与standard相似,但保留了url或者邮箱地址,也有最长字符限定配置
对英文分词有很好的支持,也有最长字符限定配置
泰文分词器
类似于滑动窗口一直遍历该字符串并截取一定长度的字符,默认最小长度(min_gram)为,最大长度(max_gram)为2,token_chars(用于确定什么类型的字符会被包含在截取后的字符,可配置的有 letter、digit等等类型,默认为包含全部类型)
和NGram相似,只不过从头开始切分,可以用作类似按头匹配的模糊查询(like a%)的效果
什么都不做,输入当输出,可以和token filter配合使用
根据java的正则表达式切分
可配置项:tokenize on chars,里面的词语作为划分的依据
simple pattern spilt tokenizer
和正则差不多
路径相关,例子
Token Filters
对于tokenizer输出的单词(term)进行增、删、修改等操作
这里由于太多了,只举几个例子
1.stop删除stop words
2.NGram和Edge NGram连词分割
3.Synonym添加近义词的term
自定义分词需要在索引的配置中设定
格式如下,type为custom区别于其他analyzer,my_custom_analyzer为自定义的分词名,char_filter为对原始文本进行处理,然后交由tokenizer进行处理,最后由filter一步步处理
PUT my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "standard",
"char_filter": [
"html_strip"
],
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
}
}
这里再用client的方式写一个简单的例子做对比,比如为博客的内容建立一个索引,内容建立一个中文分词加过滤html和敏感词的例子,标题则用标准的tokenizer,其实ik的话stop词典可以通过线上接口或本地文件进行配置,这里仅仅配置了线上添加的词库简单例子,下面是这个索引的配置
{
"settings": {
"number_of_shards":3,
"number_of_replicas":"1",
"analysis": {
"analyzer": {
"filterhtml": {
"type": "custom",
"tokenizer": "standard",
"char_filter": ["html_strip"],
"filter": ["lowercase"]
},
"ikhtml": {
"type": "custom",
"tokenizer": "ik_max_word",
"char_filter": ["html_strip", "emoji_filter"],
"filter": ["my_stop"]
}
},
"char_filter": {
"emoji_filter": {
"type": "mapping",
"mappings": ["(づ ̄ 3 ̄)づ=>亲亲", "o(* ̄▽ ̄*)ブ=>开心"]
}
},
"filter": {
"my_stop": {
"type": "stop",
"stopwords": ["尼玛", "扑街"]
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "ikhtml",
"search_analyzer": "ik_smart"
},
"title": {
"type": "text",
"analyzer": "filterhtml"
},
"author": {
"type": "keyword"
},
"date": {
"type": "date"
}
}
}
}
首先先按ik文档指示,首先要把ik插件放到es的plugin目录下,把ik的IKAnalyzer.cfg文件里的远程更新接口添加上去,然后进行更新词典的服务端代码的编写,
@Controller
public class KeyWordController {
@GetMapping(value = "/keyword.txt")
public ResponseEntity<String> getCustomDict() throws FileNotFoundException {
// 读取字典文件
HttpHeaders httpHeaders = new HttpHeaders();
File file = new File("D:\\workhb\\demo\\src\\main\\resources\\keyword.dic");
StringBuilder builder = new StringBuilder();
Scanner sc=new Scanner(file);
while(sc.hasNext()){
builder.append(sc.next()+"\n");
}
httpHeaders.set("Last-Modified", String.valueOf(builder.length()));
httpHeaders.set("ETag", String.valueOf(builder.length()));
// 返回数据
return new ResponseEntity(builder.toString(), httpHeaders, HttpStatus.OK);
}
@GetMapping(value = "/stopword.txt")
public ResponseEntity<String> stopword() throws FileNotFoundException {
// 读取字典文件
HttpHeaders httpHeaders = new HttpHeaders();
File file = new File("D:\\workhb\\demo\\src\\main\\resources\\stopword.dic");
StringBuilder builder = new StringBuilder();
Scanner sc=new Scanner(file);
while(sc.hasNext()){
builder.append(sc.next()+"\n");
}
httpHeaders.set("Last-Modified", String.valueOf(builder.length()));
httpHeaders.set("ETag", String.valueOf(builder.length()));
// 返回数据
return new ResponseEntity(builder.toString(), httpHeaders, HttpStatus.OK);
}
}
然后在配置文件上添加远程访问的地址,这里我用了花生壳的内网穿透映射到本机来改
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://1n9178z595.goho.co/keyword.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<entry key="remote_ext_stopwords">http://1n9178z595.goho.co/stopword.txt</entry>
</properties>
接下来看一下效果
post请求后localhost:9200/article/_analyze
{
"field":"content",
"text":"青青草原的羊羊子孙你们好棒,不要说扑街和尼玛(づ ̄ 3 ̄)づ"
}
就被解析如下,这代表了分词后的结果,然后出现过滤了之前定义的一些stopword和把表情符号变为自定义的符号
{
"tokens": [
{
"token": "青青草原",
"start_offset": 0,
"end_offset": 4,
"type": "CN_WORD",
"position": 1
},
{
"token": "青青草",
"start_offset": 0,
"end_offset": 3,
"type": "CN_WORD",
"position": 2
},
{
"token": "青青",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 3
},
{
"token": "青草",
"start_offset": 1,
"end_offset": 3,
"type": "CN_WORD",
"position": 4
},
{
"token": "草原",
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 5
},
{
"token": "的",
"start_offset": 4,
"end_offset": 5,
"type": "CN_CHAR",
"position": 6
},
{
"token": "羊",
"start_offset": 5,
"end_offset": 6,
"type": "CN_CHAR",
"position": 7
},
{
"token": "羊",
"start_offset": 6,
"end_offset": 7,
"type": "CN_CHAR",
"position": 8
},
{
"token": "子孙",
"start_offset": 7,
"end_offset": 9,
"type": "CN_WORD",
"position": 9
},
{
"token": "你们",
"start_offset": 9,
"end_offset": 11,
"type": "CN_WORD",
"position": 10
},
{
"token": "好棒",
"start_offset": 11,
"end_offset": 13,
"type": "CN_WORD",
"position": 11
},
{
"token": "不要",
"start_offset": 14,
"end_offset": 16,
"type": "CN_WORD",
"position": 12
},
{
"token": "要说",
"start_offset": 15,
"end_offset": 17,
"type": "CN_WORD",
"position": 13
},
{
"token": "和",
"start_offset": 19,
"end_offset": 20,
"type": "CN_CHAR",
"position": 15
},
{
"token": "亲亲",
"start_offset": 22,
"end_offset": 30,
"type": "CN_WORD",
"position": 17
}
]
}
这时候访问另一个接口,添加词组,然后再次解析:
发现在原有基础上出现这个词组,在搜索的时候,不同的分词器可能会导致不同的得分情况,分词的结果与否会影响搜索匹配与否
client api中的是验证分词的方法,用来验证分词结果,而创建索引时使用分词的话,则按照文档定义好需要的json数据