ElasticSearch底层相关原理

一、ElasticSearch文档分值_score计算底层原理

查询的时候会对搜索到的文档进行打分(filter方式不会打分)。

1.boolean model

        根据用户的query条件,先过滤出包含指定term的doc,这一步是不会打分的。

2.relevance score算法

        [ˈreləvəns] 相关度

        简单来说,就是计算出,一个索引中的文本,与搜索文本,他们之间的关联匹配程度

Elasticsearch使用的是 term frequency/inverse document frequency算法,简称为TF/IDF算法,ES底层是基于lucence,而lucence使用的算法:TF/IDF算法。

Term frequency:搜索文本中的各个词条在field文本中出现了多少次,出现次数越多,就越相关

#搜索请求:hello world

doc1:hello you, and world is very good

doc2:hello, how are you

# doc1更相关

Inverse document frequency:搜索文本中的各个词条在整个索引的所有文档中出现了多少次,出现的次数越多,就越不相关。不是针对单个文档了,是针对整个索引库中的文档。(越多越不相关,可以这样理解:主要过滤一些通用词,像:的,啊之类的,所以说是反转。)

# 搜索请求:hello world
# 情形:在index中有1万条document,hello这个单词在所有的document中,一共出现了1000次;
# world这个单词在所有的document中,一共出现了100次

doc1:hello, tuling is very good

doc2:hi world, how are you

# 这个时候doc2的相关度分数要高一些,因为doc1含有hello这个单词

Field-length norm:(field长度)field越长,相关度越弱。

# 搜索请求:hello world

doc1:{ "title": "hello article", "content": "...... N个单词" }

doc2:{ "title": "my article", "content": "...... N个单词,hi world" }

# doc1分数要高,命中内容的title字段更短。
# doc2是在文章内容中(好几百个词)才匹配上了一个world这个单词;
# 而doc1是在title中(几个单词的长度)就匹配上了一个词。

3.分析一个document上的_score是如何被计算出来的

# _explain:关键字查询打分详情
GET /es_db/_doc/1/_explain
{
  "query": {
    "match": {
      "remark": "java developer"
    }
  }
}

打分机制牵扯到空间向量模型:

  • query vector:查询向量
  • doc vector:文档向量

多个term对一个doc的总分数分析:

Query Vector:

        hello world --> es会根据hello world在所有doc中的评分情况,计算出一个query vector

        hello这个term,假设基于所有doc算出的一个评分就是3

        world这个term,假设基于所有doc算出的一个评分就是6

        那么[3, 6]就是这次查询的query vector的评分。

 Doc Vector:

        假设根据hello world查询出3个doc,一个包含hello,一个包含world,一个包含hello 以及 world

        doc1:包含hello -->那么这个doc的文档向量就是 [3, 0]

        doc2:包含world --> 那么这个doc的文档向量就是[0, 6]

        doc3:包含hello, world -->那么这个doc的文档向量就是 [3, 6]

        即会给每一个doc,拿每个term计算出一个分数来,hello有一个分数,world有一个分数,再拿所有term的分数组成一个doc vector。

匹配度:

        画在一个图中,取每个doc的doc vector对和query vector之间的的弧度,基于这个弧度计算出出每个doc对多个term的总分数。

        底层lucence的算法是很复杂的,利用空间向量模型表现在图上就是弧度越大,分数越低; 弧度越小,分数越高。如下图 :(红色的为doc vector,黑色的为query vector) 

                        

                       

        但是这只是两个关键词hello和world,但是用户不可能只输入两个关键词,可能会有多个关键词,这样使用二维图就不好表现了,只能使用线性代数来计算,无法用图来表示,脑中可以理解为一个空间立体模型,但是也是越近也匹配。但这里就只用二维坐标图做个演示。

二、分词器工作流程

1.分词器工作流程

    分词器是es中专门处理分词的组件,成为Analyzer,它的组成如下:

  •     Character Filters:针对原始文本进行处理,比如说去除html标记符
  •     Tokenizer:将原始文本按照一定规则切分成单词
  •     Token Filters:针对Tokenizer的单词进行再加工,比如说转小写、删除或新增等处理

        1)character filter:在一段文本进行分词之前,先进行预处理(标签处理、特殊字符转换)。比如说最常见的就是,过滤html标签(<span>hello<span> --> hello),& --> and(I&you --> I and you)

        2)tokenizer:分词,hello you and me --> hello, you, and, me

        3)token filter:lowercase(转小写),stop word(停用词处理),synonymom(同义词处理)。如:liked --> like,Tom --> tom,a/the/an --> 干掉停用词,small --> little

2.内置分词器

因为ES不是国内开发的,内置分词器一般可以适用于英文环境,但是中文环境一般是不适用的。

        1)standard analyzer:set, the, shape, to, semi, transparent, by, calling, set_trans, 5(默认的是standard)

        2) simple analyzer:set, the, shape, to, semi, transparent, by, calling, set, trans

        3)whitespace analyzer:Set, the, shape, to, semi-transparent, by, calling, set_trans(5)

         4)stop analyzer:移除停用词,比如a the it等等

# 分词器分词分析
POST _analyze
{
    "analyzer":"standard",
    "text":"Set the shape to semi-transparent by calling set_trans(5)"
}

3.定制化分词器

PUT /my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        # es_std代表当前索引库自定义分词器的名称
        "es_std": {
          # 代表在standard分词器基础上做的变更、优化、升级
          "type": "standard",
          # 在英文模式下过滤停用词,比如a、an、the
          "stopwords": "_english_"
        }
      }
    }
  }
}

# 如果还是使用standard这个分词器进行分析的话,则a、an、the这类词也会被拆分
GET /my_index/_analyze
{
  "analyzer": "standard", 
  "text": "a dog is in the house"
}


# 如果使用了我们改良后的分词器,则a、an、the等词不会做分词
# 注意使用索引库的自定义分词器的时候,只能在当前索引库使用
GET /my_index/_analyze
{
  # 使用我们改良后的分词器
  "analyzer": "es_std",
  "text":"a dog is in the house"
}

上面的例子,只是使用ES内置的一些关键字对分词器做一些小的改动,我们也可以使用下面的方式来进行优化分词器,更大程度满足我们的业务需求。

某个索引库的自定义分词器,只试用于所声明的索引库,否则在所声明的索引库外的索引库使用的话会报错:找不到分词器

PUT /my_index
{
  "settings": {
    "analysis": {
      "char_filter": {
        # &_to_and是字符过滤器的名称,可以随便起
        "&_to_and": {
          # 映射类型
          "type": "mapping",
          # 具体的映射规则:如果含有&则转为and
          "mappings": [
            "&=> and"
          ]
        }
      },
      "filter": {
        # 停用词过滤器的名称,可以随表起
        "my_stopwords": {
          # 停用词类型
          "type": "stop",
          # 指出具体的停用词
          "stopwords": [
            "the",
            "a"
          ]
        }
      },
      "analyzer": {
        # 定制化分词器的名称:my_analyzer
        "my_analyzer": {
          # 表示自定义类型
          "type": "custom",
          # 定制化体现的地方,表示使用上方定义的规则
          "char_filter": [
            # 是ES内置的内容,专门处理html标签
            "html_strip",
            "&_to_and"
          ],
          # 代表当前定制化的分词器是在standard分词器的基础上做的扩展、优化
          "tokenizer": "standard",
          # 定制化体现的地方,表示使用上方定义的规则
          "filter": [
            # 是ES内置的内容,转为小写
            "lowercase",
            "my_stopwords"
          ]
        }
      }
    }
  }
}
GET /my_index/_analyze
{
  "text": "tom&jerry are a friend in the house, <a>, HAHA!!",
  "analyzer": "my_analyzer"
}

# 执行效果
{
  "tokens" : [
    {
      "token" : "tomandjerry",
      "start_offset" : 0,
      "end_offset" : 9,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "are",
      "start_offset" : 10,
      "end_offset" : 13,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "friend",
      "start_offset" : 16,
      "end_offset" : 22,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "in",
      "start_offset" : 23,
      "end_offset" : 25,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "house",
      "start_offset" : 30,
      "end_offset" : 35,
      "type" : "<ALPHANUM>",
      "position" : 6
    },
    {
      "token" : "haha",
      "start_offset" : 42,
      "end_offset" : 46,
      "type" : "<ALPHANUM>",
      "position" : 7
    }
  ]
}

4.IK分词器详解

ik配置文件地址:es/plugins/ik/config目录,下面是一些IK的配置文件的说明:

  • IKAnalyzer.cfg.xml:用来配置自定义词库
  • main.dic:ik原生内置的中文词库,总共有27万多条,只要是这些单词,都会被分在一起
  • quantifier.dic:放了一些单位相关的词
  • suffix.dic:放了一些后缀
  • surname.dic:中国的姓氏
  • stopword.dic:英文停用词

ik原生最重要的两个配置文件:

        main.dic:包含了原生的中文词语,会按照这个里面的词语去分词

        stopword.dic:包含了英文的停用词,一般,像停用词,会在分词的时候,直接被干掉,不会建立在倒排索引中

(1)IKAnalyzer.cfg

<properties>
	<comment>IK Analyzer 扩展配置</comment>
	<!--用户可以在这里配置自己的扩展字典 -->
	<entry key="ext_dict">location</entry>
	 <!--用户可以在这里配置自己的扩展停止词字典-->
	<entry key="ext_stopwords">location</entry>
	<!--用户可以在这里配置远程扩展字典 -->
	<entry key="remote_ext_dict">words_location</entry> 
	<!--用户可以在这里配置远程扩展停止词字典-->
	<entry key="remote_ext_stopwords">words_location</entry>
</properties>

      

(1)自己建立扩展词库:每年都会涌现一些特殊的流行词,网红,蓝瘦香菇,喊麦,鬼畜,一般不会在ik的原生词典里,那也就不会对这次词语进行分词,建立倒排索引,则自己补充自己的最新的词语,到ik的词库里面去。补充自己的词语,然后需要重启es才能生效。

# 假设custom在config目录下
<entry key="ext_dict">custom/mydict.dic</entry>

(2)自己建立停用词库:比如了,的,啥,么,我们可能并不想去建立索引让人家搜索到。已经有了常用的中文停用词,可以根据业务要求补充自己的停用词,然后重启es才能生效

<entry key="ext_stopwords">custom/ext_stopword.dic</entry>

 (扩展词的格式就是和ik中的dic的格式一样,一行一个词就可以了)

思考:如果像上面进行扩展词典,每次都是在es的扩展词典中,手动添加新词语,很坑。

        1)每次添加完,都要重启es才能生效,非常麻烦。

        2)es是分布式的,可能有数百个节点,你不能每次都一个一个节点上面去修改。

(2)IK热更新

主要解决问题:实现es不停机的情况下,我们可以随意动态修改我们的扩展词库,并使ES识别到我们做的修改。

        1)远程扩展词典:把我们的字典放到远程一个tomcat服务器中,假设我们把文件放到tomcat的ROOT文件夹下,则可以这样配置:

<entry key="remote_ext_dict">http://ip:port/host.dic</entry> 
<entry key="remote_ext_stopwords">http://ip:port/stop.dic</entry>

只要配置了这个远程字典的key(remote_ext_dict、remote_ext_stopwords),ik读到并识别到了这个key的配置,就会开启一个定时器,会定时去加载这个文件的内容。这样每次修改就修改远程服务器上的文件就行了,相当于实现了热更新,但是也会有一些不方便。但是这种方式官方也不推荐,不稳定。

        2)使用另一种方式,使用mysql,需要修改ik的源码。

之后补充

三、高亮显示

1.高亮显示

百度会把搜索到的内容,会把关键字做成高亮的效果,这样也比较醒目,比如搜一个文章,如果没有高亮,文章内容这么多,我们怎么知道文章包含我们搜索的内容。

em标签会把关键字变红,如果不喜欢这个颜色,可以指定别的标签,都是可以指定的。

# 定义一个索引库
PUT /news_website
{
  "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word"
        }
      }
    }
  
}

# 插入测试数据
PUT /news_website/_doc/1
{
  "title": "这是我写的第一篇文章",
  "content": "大家好,这是我写的第一篇文章,特别喜欢这个文章门户网站!!!"
}

# 我们查询title中含有“文章”的文档,并高亮显示
GET /news_website/_doc/_search 
{
  "query": {
    "match": {
      "title": "文章"
    }
  },
  # 高亮配置
  "highlight": {
    "fields": {
      # title中的关键字高亮
      "title": {}
    }
  }
}
# 执行结果
{
  "took" : 32,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    # 返回的原始数据
    "hits" : [
      {
        "_index" : "news_website",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "title" : "这是我写的第一篇文章",
          "content" : "大家好,这是我写的第一篇文章,特别喜欢这个文章门户网站!!!"
        },
        # 这里会返回高亮的部分
        # em标签会把关键字变红,如果不喜欢这个颜色,查询的时候可以指定别的标签,都是可以指定的。
        "highlight" : {
          "title" : [
            "这是我写的第一篇<em>文章</em>"
          ]
        }
      }
    ]
  }
}

只会把包含在查询条件中并且在高亮配置中指明的字段中的关键字做成高亮。如果查询条件中没有某个字段,即使你在高亮相关语法中指明要做成高亮,也是不起作用的。

# 这种写法content中的关键字是不会被高亮显示的
GET /news_website/_doc/_search 
{
  "query": {
    "match": {
      "title": "文章"
    }
  },
  "highlight": {
    "fields": {
      "title": {}
      "content": {}
    }
  }
}

# 只有把content也作为查询条件,并且在highlight中指定高亮,才会将content中的关键字的高亮展示
GET /news_website/_doc/_search 
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": "文章"
          }
        },
        {
          "match": {
            "content": "文章"
          }
        }
      ]
    }
  },
  "highlight": {
    "fields": {
      "title": {},
      "content": {}
    }
  }
}

2.常用的highlight(高亮方式)介绍

  • plain highlight:ucene highlight默认
  • posting highlight:index_options=offsets
  • fast vector highlight:term_vector=with_positions_offsets

(1)plain和posting对比:

        1)posting性能比plain highlight要高,因为不需要重新对高亮文本进行分词

        2)对磁盘的消耗更少

(2)posting highlight

在创建索引映射的时候,如果在相应字段中配置了index_option之后,那么在进行高亮查询的时候,这个字段的高亮方式会采用posting highlight高亮方式。

PUT /news_website
{
  "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          # 做高亮搜索的时候会采用posting高亮方式
          "index_options": "offsets"
        }
      }
  }
}

(3)fast vector highlight

对大field而言(大于1mb),性能更高

PUT /news_website
{
  "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          # 指定使用fast vector高亮方式
          "term_vector" : "with_positions_offsets"
        }
      }
  }
}

(4)强制使用某种highlighter

如果一开始在映射中指定了使用哪种高亮方式,但我们在实际查询中也可以强制使用其他的高亮方式。

GET /news_website/_doc/_search 
{
  "query": {
    "match": {
      "content": "文章"
    }
  },
  "highlight": {
    "fields": {
      "content": {
        # 代表强制使用plain默认的高亮方式
        "type": "plain"
      }
    }
  }
}

(6)高亮片段fragment的设置(展示最优片段大小)

举个例子:百度搜索的时候会展示标题和内容等信息,但是内容又不把所有内容都显示出来,只显示包含关键字的一小部分就行,即最优的片段;标题中可能只显示20个字就行了等等。那么这个最优的片段需要多少文字,都可以自己去声明。

GET /_search
{
    "query" : {
        "match": { "content": "文章" }
    },
    "highlight" : {
        "fields" : {
            "content" : {"fragment_size" : 150, "number_of_fragments" : 3 }
        }
    }
}
  • fragment_size: 设置要显示出来的fragment文本判断的长度,即一个最优的片段需要多少文字,默认是100。假设你的一个Field的值的长度是1万,但是你不可能在页面上把内容都显示出来,那么就可以使用这个属性声明。
  • number_of_fragments:你想要你的高亮的fragment文本片段有多个片段,你就可以指定显示几个片段。

(7)设置高亮html标签

默认是<em>标签,关键字展示为红色,你也可以修改展示样式:

GET /news_website/_doc/_search 
{
  "query": {
    "match": {
      "content": "文章"
    }
  },
  "highlight": {
    # 包裹关键字的前置标签,这里指明css属性color为红色,即字体为红色
    "pre_tags": ["<span color='red'>"],
    # 包裹关键字的后置标签
    "post_tags": ["</span>"], 
    "fields": {
      "content": {
        "type": "plain"
      }
    }
  }
}

(5)总结

        其实可以根据你的实际情况去考虑,一般情况下,用plain highlight也就足够了,不需要做其他额外的设置;如果对高亮的性能要求很高,可以尝试启用posting highlight;如果field的值特别大,超过了1M,那么可以用fast vector highlight。