elasticsearch-映射和分析
elasticsearch-映射和分析
映射和分析
- 查看索引映射分析
/gb/_mapping
- 全文搜索和精确字段搜索
/gb/_search?q=2014-09-15
/gb/_search?q=date:2014-09-15
精确字段搜索和全文搜索是搜索引擎和其它数据库的本质区别
精确值VS全文
- es中的数据可以概括的分为两类:精确值和全文
- 精确值:例如日期或者用户id, 字符串也可以表示精确值,例如用户名或者邮箱地址,
对于精确值来说,Foo和foo是不同的,2014和2014-09-15也是不同的 - 全文:是指文本数据,例如一个推文的内容或者一封邮件的内容
全文通常是指非结构化的数据 - 精确值很容易查询,一般用sql就可以直接查,但是查询全文数据要微妙的多
我们问的不只是这个文档匹配查询吗,而是该文档匹配程度有多大,换句话说,该文档与给出的查询相关性如何 - 我们很少对全文类型的域做精确匹配,相反,我们希望在文本类型的域中做搜索,不仅如此,
我们还希望搜索能够理解我们的意图- 搜索 UK ,会返回包含 United Kindom 的文档。
- 搜索 jump ,会匹配 jumped , jumps , jumping ,甚至是 leap 。
- 搜索 johnny walker 会匹配 Johnnie Walker , johnnie depp 应该匹配 Johnny Depp 。
- fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。
为了促进这类在全文域中的查询,es首先分析文档,然后根据结果创建倒排索引,接下来讨论倒排索引和分析过程
倒排索引
- es使用一种称为 倒排索引 的结构,它适用于快速全文搜索,一个倒排索引由文档中所有不重复词的列表组成
对于其中的每个词,有一个包含它的文档列表 - 假设我们有两个文档,每个文档的content域内容如下
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引我们需要将每个文档的content域拆分成单独的词(我们称之为词条或tokens),
创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档,
Term Doc_1 Doc_2
Quick | | X
The | X |
brown | X | X
dog | X |
dogs | | X
fox | X |
foxes | | X
in | | X
jumped | X |
lazy | X | X
leap | | X
over | X | X
quick | X |
summer | | X
the | X |
现在,如果我们先搜索 quick brown,我们只需要查找包含每个词条的文档
Term Doc_1 Doc_2
brown | X | X
quick | X |
Total | 2 | 1
两个文档都匹配,但是第一个比第二个匹配度更高,如果我们仅适用词条匹配数量相似性算法
第一个文档比第二个文档更佳
但是也有一些问题,
Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。
fox 和 foxes 非常相似, 就像 dog 和 dogs ;他们有相同的词根。
jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。
3. 文档中多个字段and查询
/gb,us/_search
{
"query": {
"bool": {
"must": [
{"match_phrase": {"name": "mary"}},
{"match_phrase": {"tweet": "however"}}
]
}
}
}
- 这非常重要,你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式
分析和分析器
- 分析包含下面的过程
首先,将一块文本分成适合于倒排索引的独立的词条
之后,将这些词条统一化为标准格式提高它们的可搜索性,或者recall - 分析器执行上面的工作实际上是将三个功能包在了一起
- 字符过滤器
首先字符串按顺序通过每个字符过滤器,它们的任务是在分词前整理字符串,一个字符过滤器可以用来去掉HTML,
或者将&转化为and - 分词器
字符串被分词器分为单个的词条,一个简单的分词器在遇到空格和标点的时候,可能会将文本拆分成词条 - token过滤器
词条按照顺序通过每个token过滤器,这个过程可能会改变词条(大写转小写),
删除词条(例如像a and the等无用词),或者增加词条(向jump和leap这种同义词)
- es提供了开箱即用的字符过滤器、分词器、token过滤器,这些可以组合起来形成自定义的分析器,用于不同的目的,
-
内置分析器
es还提供了可以直接使用的预包装的分析器,接下来看看最重要的分析器,为了证明它们的差异
我们看看每个分析器会从下面的字符串得到哪些词条
Set the shape to semi-transparent by calling set_trans(5)
3.1 标准分析器
标准分析器是es默认使用的分析器,它是分析各种语言文本最常用的选择,他根据unicode联盟定义的单词边界划分文本
删除绝大部分标点,最后将词条小写,它会产生
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
3.2 简单分析器
简单分析器是在任何不是字母的地方分割文本,它会产生
set, the, shape, to, semi, transparent, by, calling, set, trans
3.3 空格分析器
空格分析器在空格的地方划分文本,它会产生
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
3.4 语言分析器
特定语言分析器可用于很多语言,它们可以考虑指定语言的特点,英语分析器附带了一组英语无用词,它们会直接被删除
由于该分析器理解英语语法的规则,这个分词器可以提取英语单词的词干
英语分词器会产生下面的词条
set, shape, semi, transpar, call, set_tran, 5
注意看 transparent、 calling 和 set_trans 已经变为词根格式。 -
什么时候使用分析器
当我们索引一个文档时,它的全文域被分析成词条用来创建倒排索引,但是,当我们在全文域进行搜索的时候
也需要将查询字符串通过相同的分析过程,以保证我们查询的词条格式和索引中的词条格式一致
- 当你查询一个全文域时,会对查询字符串应用相同的分析器,以产生正确的搜索词条列表
- 当你查询一个精确值域时,不会分析查询字符串,而是搜索你指定的精确值
再看之前的问题,date是精确值域:单独的词条,2014-05-19,所以GET /_search?q=date:2014是查不到的
- 测试分析器
有时候很难理解分词的过程和存储到索引中的词条,为了理解发生了什么
我们可以使用analyze API来看文本是如何被拆分的,在消息体里,指定分析器和要分析的文本
GET /_analyze
{
"analyzer": "standard",
"text": "Text to analyze"
}
结果中,每个元素代表一个单独的词条
{
"tokens": [
{
"token": "text",
"start_offset": 0,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "to",
"start_offset": 5,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "analyze",
"start_offset": 8,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 2
}
]
}
token是实际存储到索引中的词条,position指明词条在原始文本中出现的位置,
start_offset和end_offset指明词条在原始字符串中的位置
每个分析器的type值都不一样,可以忽略它们,它们在Elasticsearch中的唯一作用在于keep_types token 过滤器
- analyze API是一个有用的工具,它有助于我们理解es索引内部发生了什么,后面会进一步讨论
- 指定分析器
当es在你的文档中检测到一个新的字符串域,它会自动设置其为一个全文字符串域,使用标准分析器对它进行分析
当我们不希望总是这样,希望使用一个不同的分析器,适用于我们的数据使用的语言,有时候你想一个字符串域
就是一个字符串域,不使用分析,直接索引你传入的精确值,例如用户Id或者一个内部的状态域或标签
要做到这一点我们就必须手动指定这些域的映射
映射
- 为了将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串,es需要知道每个域中的数据类型
这个信息包含在映射中
索引中每个文档都有类型,每种类型都有它自己的映射, - 核心简单域类型
- es支持如下简单域类型
- 字符串 text
- 整数 byte、short、integer、long
- 浮点数 float double
- 布尔型 boolean
- 日期 date
-
当你索引一个包含新域的文档--之前未曾出现,es会使用动态映射,
通过json中节本数据类型,尝试猜测域类型
这意味着如果你想通过"13"索引一个数字,它会被映射为text类型,而不是long,
但是如果这个域已经被映射为long,那么es会尝试将该字符串转换为long类型,如果无法转换,则抛出一个异常 -
查看映射
通过 /索引/_mapping,我们可以查看es中一个或多个索引的映射
es根据我们索引的文档,为域(称为属性)动态生成的映射
- 错误的映射,例如将age域映射为string类型,而不是integer或long,会导致查询出现令人困惑的结果,
- 自定义域映射
尽管很多情况下基本域数据类型已经够用,但你经常需要为单独域自定义映射,特别是字符串域
自定义映射允许你执行下面的操作
- 全文字符串域与精确值字符串域的区别
- 使用特定语言分析器
- 优化域以适应部分匹配
- 指定自定义数据格式
- 。。。
域最重要的属性是type,对于不是text的域,你一般只需要设置type
默认text类型域会被认为包含全文,也就是说它们的值在索引前,会通过一个分析器,
针对于这个域的查询在搜索前也会经过这个分析器
增加一个tag2域,index选择为false
{
"properties": {
"tag2": {
"type": "text",
"index": false
}
}
}
也就是说tag2的域是不能被索引的查询的
- text域映射的两个最重要属性index、analyzer
- index属性控制怎样索引字符串: true、false
true:索引这个域,false:不索引这个域
text域的index属性默认值是true,如果我们想不索引这个域,我们可以设置它为false - analyzer
对于analyzer属性指定在搜索时和索引时使用的分析器,es默认使用standard分析器,
但是也可以指定一个内置的分析器替代它:simple、whitespace、english
- 更新映射
- 创建索引,指定域的类型和tweet域使用的分析器
PUT /gb
{
"mappings": {
"properties": {
"tweet": {
"type": "text",
"analyzer": "english"
},
"date": {
"type": "date"
},
"name": {
"type": "text"
},
"user_id": {
"type": "long"
}
}
}
}
通过消息体中指定的mappings创建了索引,
稍后我们在gb索引中增加一个新名为tag的type类型为keyword不分词文本域,使用_mapping
PUT /gb/_mapping
{
"properties": {
"tag": {
"type": "keyword"
}
}
}
注意:我们不需要再次列出所有已经存在的域,因为无论如何我们都无法改变它们,
新域已经被合并到了存在的映射中
- 测试映射
GET /gb/_analyze
{
"field": "name",
"text": "a b c "
}
{
"field": "tag",
"text": "a b c "
}
消息体里传输我们想要分析的文本
name域产生三个词条a、b、c,tag域产生一个词条“a b c ”
换句话说,我们的映射正常工作
- 分析器使用whitespace的映射情况
GET /test01/_analyze
{
"field": "tag3",
"text": "abc-def aa ddef"
}
会分析映射成三个token, abc-def aa ddef
复杂核心域类型
- 除了我们提提到的简单标量数据类型,json还有null值、数组和对象,这些es都支持
- 多值域
- 很有可能,我们希望tag域包含多个标签,我们可以以数组的形式索引标签
{ "tag": [ "search", "nosql" ]}
对于数组没有特殊的映射需求,任何域都可以包含0、1或者多个值,就像全文域分析得到多个词条
这暗示,数组中的所有值都必须是相同的数据类型,不能将日期和字符串混在一起,
如果通过索引数组来创建新的域,es会用数组中第一个值的数据类型作为这个域的类型
注意:当你从es得到一个文档,每个数组的顺序和你当初索引文档时一样,你得到的_source域
和当初索引文档时一模一样的json - 但是数组是以多值域索引的-可以搜索,但是无序的,在搜索的时候,不能指定第一个或者最后一个
更确切的说,把数组想象成装在袋子里的值
- 空域
- 当然数组可以为空,这相当于存在零值,事实上,在lucene中不能存储null值,
所以我们认为存null值的域为空域,下面3种域被认为是空域,将不会被索引
{
"null_value": null,
"empty_array": [],
"array_with_null_value": [null]
}
- 多层级对象
我们讨论的最后一个json原生数据类型是对象,在其它语言中称为哈希 哈希map 字典 关联数组
内部对象经常用来嵌入一个实体或对象到其它对象中,例子
{
"tweet": "Elasticsearch is very flexible",
"user": {
"id": "@johnsmith",
"gender": "male",
"age": 26,
"name": {
"full": "John Smith",
"first": "John",
"last": "Smith"
}
}
}
- 内部对象的映射
es会动态检测新的对象并映射它们为对象,在properties属性下列出内部域
最外层的是根对象,里面的是内部对象
- user和name域的映射结构与tweet类型的相同,tweet称为根对象,除了它有一些文档元数据的顶级域外
例如_source域,和其它内部对象一样
- 内部对象是如何索引的
lucene不理解内部对象,lucene文档是由一组键值对列表组成的,为了能让es有效的索引内部类
它把我们的文档转化成这样
{
"tweet": [elasticsearch, flexible, very],
"user.id": [@johnsmith],
"user.gender": [male],
"user.age": [26],
"user.name.full": [john, smith],
"user.name.first": [john],
"user.name.last": [smith]
}
- 多级嵌套查询(内部对象数据)
要查看多级嵌套查询的工作方式,首先需要一个具有嵌套字段的索引,下面的请求用嵌套的make和model
字段定义驱动程序索引的映射
- 添加一个drivers索引
PUT /drivers - 为drivers索引添加域的映射类型
PUT /drivers/_mapping
{
"properties": {
"driver": {
"type": "nested",
"properties": {
"last_name": {
"type": "text"
},
"vehicle": {
"type": "nested",
"properties": {
"make": {
"type": "text"
},
"model": {
"type": "nested"
}
}
}
}
}
}
}
- 接下来索引一些文档
PUT /drivers/_doc/
{
"driver" : {
"last_name" : "Hudson",
"vehicle" : [
{
"make" : "Mifune",
"model" : "Mach Five"
},
{
"make" : "Miller-Meteor",
"model" : "Ecto-1"
}
]
}
}
{
"driver" : {
"last_name" : "McQueen",
"vehicle" : [
{
"make" : "Powell Motors",
"model" : "Canyonero"
},
{
"make" : "Miller-Meteor",
"model" : "Ecto-1"
}
]
}
}
现在可以使用多级嵌套查询,根据make和model字段匹配文档
GET /drivers/_search
{
"query": {
"nested": {
"path": "driver",
"query": {
"nested": {
"path": "driver.vehicle",
"query": {
"match": {
"driver.vehicle.make": "mifune"
}
}
}
}
}
}
}
正常返回响应,非常强大啊!!!
参考文档
- es多个值作为关键字搜索(相当于关系型数据库中的in查询)
GET /test/_search
{
"query": {
"bool": {
"must": [
{
"terms": {
"tags": [11, 66]
}
}
]
}
}
}
- 内部对象数据是如何被索引的
最后,考虑包含内部对象的数组是如何被索引的。 假设我们有个 followers 数组:
{
"followers": [
{ "age": 35, "name": "Mary White"},
{ "age": 26, "name": "Alex Jones"},
{ "age": 19, "name": "Lisa Smith"}
]
}
这个文档会像我们之前描述的那样被扁平化处理,结果如下所示:
{
"followers.age": [19, 26, 35],
"followers.name": [alex, jones, lisa, smith, mary, white]
}
{age: 35} 和 {name: Mary White} 之间的相关性已经丢失了,因为每个多值域只是一包无序的值,而不是有序数组。这足以让我们问,“有一个26岁的追随者?”
但是我们不能得到一个准确的答案:“是否有一个26岁 名字叫 Alex Jones 的追随者?”
相关内部对象被称为 nested 对象,可以回答上面的查询,我们稍后会在嵌套对象中介绍它。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)