elasticsearch-映射和分析

elasticsearch-映射和分析

映射和分析

  1. 查看索引映射分析
    /gb/_mapping
  2. 全文搜索和精确字段搜索
/gb/_search?q=2014-09-15
/gb/_search?q=date:2014-09-15

精确字段搜索和全文搜索是搜索引擎和其它数据库的本质区别

精确值VS全文

  1. es中的数据可以概括的分为两类:精确值和全文
  2. 精确值:例如日期或者用户id, 字符串也可以表示精确值,例如用户名或者邮箱地址,
    对于精确值来说,Foo和foo是不同的,2014和2014-09-15也是不同的
  3. 全文:是指文本数据,例如一个推文的内容或者一封邮件的内容
    全文通常是指非结构化的数据
  4. 精确值很容易查询,一般用sql就可以直接查,但是查询全文数据要微妙的多
    我们问的不只是这个文档匹配查询吗,而是该文档匹配程度有多大,换句话说,该文档与给出的查询相关性如何
  5. 我们很少对全文类型的域做精确匹配,相反,我们希望在文本类型的域中做搜索,不仅如此,
    我们还希望搜索能够理解我们的意图
    • 搜索 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首先分析文档,然后根据结果创建倒排索引,接下来讨论倒排索引和分析过程

倒排索引

  1. es使用一种称为 倒排索引 的结构,它适用于快速全文搜索,一个倒排索引由文档中所有不重复词的列表组成
    对于其中的每个词,有一个包含它的文档列表
  2. 假设我们有两个文档,每个文档的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"}}
            ]
        }
    }
}
  • 这非常重要,你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式

分析和分析器

  1. 分析包含下面的过程
    首先,将一块文本分成适合于倒排索引的独立的词条
    之后,将这些词条统一化为标准格式提高它们的可搜索性,或者recall
  2. 分析器执行上面的工作实际上是将三个功能包在了一起
  • 字符过滤器
    首先字符串按顺序通过每个字符过滤器,它们的任务是在分词前整理字符串,一个字符过滤器可以用来去掉HTML,
    或者将&转化为and
  • 分词器
    字符串被分词器分为单个的词条,一个简单的分词器在遇到空格和标点的时候,可能会将文本拆分成词条
  • token过滤器
    词条按照顺序通过每个token过滤器,这个过程可能会改变词条(大写转小写),
    删除词条(例如像a and the等无用词),或者增加词条(向jump和leap这种同义词)
  • es提供了开箱即用的字符过滤器、分词器、token过滤器,这些可以组合起来形成自定义的分析器,用于不同的目的,
  1. 内置分析器
    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 已经变为词根格式。

  2. 什么时候使用分析器
    当我们索引一个文档时,它的全文域被分析成词条用来创建倒排索引,但是,当我们在全文域进行搜索的时候
    也需要将查询字符串通过相同的分析过程,以保证我们查询的词条格式和索引中的词条格式一致

  • 当你查询一个全文域时,会对查询字符串应用相同的分析器,以产生正确的搜索词条列表
  • 当你查询一个精确值域时,不会分析查询字符串,而是搜索你指定的精确值
    再看之前的问题,date是精确值域:单独的词条,2014-05-19,所以GET /_search?q=date:2014是查不到的
  1. 测试分析器
    有时候很难理解分词的过程和存储到索引中的词条,为了理解发生了什么
    我们可以使用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索引内部发生了什么,后面会进一步讨论
  1. 指定分析器
    当es在你的文档中检测到一个新的字符串域,它会自动设置其为一个全文字符串域,使用标准分析器对它进行分析
    当我们不希望总是这样,希望使用一个不同的分析器,适用于我们的数据使用的语言,有时候你想一个字符串域
    就是一个字符串域,不使用分析,直接索引你传入的精确值,例如用户Id或者一个内部的状态域或标签
    要做到这一点我们就必须手动指定这些域的映射

映射

  1. 为了将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串,es需要知道每个域中的数据类型
    这个信息包含在映射中
    索引中每个文档都有类型,每种类型都有它自己的映射,
  2. 核心简单域类型
  • es支持如下简单域类型
    • 字符串 text
    • 整数 byte、short、integer、long
    • 浮点数 float double
    • 布尔型 boolean
    • 日期 date
  1. 当你索引一个包含新域的文档--之前未曾出现,es会使用动态映射,
    通过json中节本数据类型,尝试猜测域类型
    这意味着如果你想通过"13"索引一个数字,它会被映射为text类型,而不是long,
    但是如果这个域已经被映射为long,那么es会尝试将该字符串转换为long类型,如果无法转换,则抛出一个异常

  2. 查看映射
    通过 /索引/_mapping,我们可以查看es中一个或多个索引的映射
    es根据我们索引的文档,为域(称为属性)动态生成的映射

  • 错误的映射,例如将age域映射为string类型,而不是integer或long,会导致查询出现令人困惑的结果,
  1. 自定义域映射
    尽管很多情况下基本域数据类型已经够用,但你经常需要为单独域自定义映射,特别是字符串域
    自定义映射允许你执行下面的操作
  • 全文字符串域与精确值字符串域的区别
  • 使用特定语言分析器
  • 优化域以适应部分匹配
  • 指定自定义数据格式
  • 。。。
    域最重要的属性是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
  1. 更新映射
  • 创建索引,指定域的类型和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"
        }
    }
}

注意:我们不需要再次列出所有已经存在的域,因为无论如何我们都无法改变它们,
新域已经被合并到了存在的映射中

  1. 测试映射
    GET /gb/_analyze
{
    "field": "name",
    "text": "a b c "
}
{
    "field": "tag",
    "text": "a b c "
}

消息体里传输我们想要分析的文本
name域产生三个词条a、b、c,tag域产生一个词条“a b c ”
换句话说,我们的映射正常工作

  1. 分析器使用whitespace的映射情况
    GET /test01/_analyze
{
    "field": "tag3",
    "text": "abc-def aa ddef"
}

会分析映射成三个token, abc-def aa ddef

复杂核心域类型

  1. 除了我们提提到的简单标量数据类型,json还有null值、数组和对象,这些es都支持
  2. 多值域
  • 很有可能,我们希望tag域包含多个标签,我们可以以数组的形式索引标签
    { "tag": [ "search", "nosql" ]}
    对于数组没有特殊的映射需求,任何域都可以包含0、1或者多个值,就像全文域分析得到多个词条
    这暗示,数组中的所有值都必须是相同的数据类型,不能将日期和字符串混在一起,
    如果通过索引数组来创建新的域,es会用数组中第一个值的数据类型作为这个域的类型
    注意:当你从es得到一个文档,每个数组的顺序和你当初索引文档时一样,你得到的_source域
    和当初索引文档时一模一样的json
  • 但是数组是以多值域索引的-可以搜索,但是无序的,在搜索的时候,不能指定第一个或者最后一个
    更确切的说,把数组想象成装在袋子里的值
  1. 空域
  • 当然数组可以为空,这相当于存在零值,事实上,在lucene中不能存储null值,
    所以我们认为存null值的域为空域,下面3种域被认为是空域,将不会被索引
{
  "null_value": null,
  "empty_array": [],
  "array_with_null_value": [null]
}
  1. 多层级对象
    我们讨论的最后一个json原生数据类型是对象,在其它语言中称为哈希 哈希map 字典 关联数组
    内部对象经常用来嵌入一个实体或对象到其它对象中,例子
{
    "tweet":            "Elasticsearch is very flexible",
    "user": {
        "id":           "@johnsmith",
        "gender":       "male",
        "age":          26,
        "name": {
            "full":     "John Smith",
            "first":    "John",
            "last":     "Smith"
        }
    }
}
  1. 内部对象的映射
    es会动态检测新的对象并映射它们为对象,在properties属性下列出内部域
    最外层的是根对象,里面的是内部对象
  • user和name域的映射结构与tweet类型的相同,tweet称为根对象,除了它有一些文档元数据的顶级域外
    例如_source域,和其它内部对象一样
  1. 内部对象是如何索引的
    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]
}
  1. 多级嵌套查询(内部对象数据)
    要查看多级嵌套查询的工作方式,首先需要一个具有嵌套字段的索引,下面的请求用嵌套的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"
                        }
                    }
                }
            }
        }
    }
}

正常返回响应,非常强大啊!!!
参考文档

  1. es多个值作为关键字搜索(相当于关系型数据库中的in查询)
    GET /test/_search
{
    "query": {
        "bool": {
            "must": [
                {
                    "terms": {
                        "tags": [11, 66]
                    }
                }
            ]
        }
    }
}
  1. 内部对象数据是如何被索引的
    最后,考虑包含内部对象的数组是如何被索引的。 假设我们有个 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 对象,可以回答上面的查询,我们稍后会在嵌套对象中介绍它。

参考文档

posted @ 2022-04-19 11:46  专职  阅读(56)  评论(0编辑  收藏  举报