分享

ElasticSearch 倒排索引、分词

 书屋随身带 2015-03-31

es使用称为倒排索引的结构达到快速全文搜索的目的。

一个倒排索引包含一系列不同的单词,这些单词出现在任何一个文档,

对于每个单词,对应着所有它出现的文档。

比如说,我们有2个文档,每个文档有一个conteng字段。

内容如下:

“ The quick brown fox jumped over the lazy dog”

“ Quick brown foxes leap over lazy dogs in summer”

为了创建倒排索引,

我们首先对每个字段进行分词,我们称之为terms或者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

两个文档都匹配,但是第一个文档有更高的匹配度,

如果我们采用一个简单的相似算法,我们可以说,第一个文档比第2个文档有更高的匹配度。

也更相关。

但是,仍然有一些问题。

"Quick"  "quick" 看起来是不同的单词,但是用户通常认为是一样的。

"fox"  "foxes" 更相似,还有 "dog"  "dogs" ,具有共同的词根。

"jumped"  "leap" ,尽管不具备相同的词根,意思上是一样的。它们是同义词。

对于以上的索引,一个搜索 "+Quick + fox" 不可能匹配任何文档。

单词 "Quick" 和单词 "fox" 必须在同一个文档里以满足查询要求,

但是第一个文档包括 "quick fox" 而第2个文档包含 "Quick foxes" .

我们的用户有理由希望两个文档都匹配,我们可以做的更好。

如果我们把单词归一化到标准格式,我们就可以达到上面的目标。

这种情况下,虽然单词不是完全一致,但是也足够相似保证相关性。比如:

"Quick" 可以小写为 "quick" .

"foxes" 可以提取词根成为 "fox" .

类似的 "dogs" 可以成为 "dog" .

"jumped"  "leap" 是同义词,可以索引为一个单词 "jump" .

那么,现在的索引就是:

Term Doc_1 Doc_2

-------------------------

brown | X | X

dog | X | X

fox | X | X

in | | X

jump | X | X

lazy | X | X

over | X | X

quick | X | X

summer | | X

the | X | X

------------------------

我们的搜索 "+Quick +fox" 仍然失败,因为我们不再有Quick在索引里,

尽管如此,如果我们采用同样的归一化规则,

我们可以用在查询字符串上,它就变为 "+quick +fox" ,

这样就可以匹配到文档。

这一点非常重要,你只能找到那些在你的索引里出现的单词,所以,索引过的文本和查询字符串都需要遵循同一种归一化规则。

分词和归一化叫做分析http://my.oschina.net/qiangzigege/blog/265360

什么叫分析过程?

)将文本块分词,以倒排索引的方式

)归一化到标准形式来提高可搜索性。

这个工作的执行者叫做分析器,一个分析器包含以下 个功能:

)字母过滤

首先,字符串依次通过任何一个字符过滤器,

过滤器的工作是清洗字符串,也就是说先清洗再分词。

一个字符过滤器可以去除HTML字符,也可以转换 "&" 变为 "and" .

)分词

下一步,字符串被分词为很多个单词,一个简单的分词器也许依靠空格或者标点符号来分词。

)单词过滤

最后,每个单词传给单词过滤器,它们可以将单词小写或者删除单词比如a,and,the,etc.

或者增加单词比如jump和leap.

es提供了很多字符串过滤器,分词器和单词过滤器。

这些单元可以组合起来使用,后面再说。

内置的分析器

尽管如此,es也包含了很多内置的分词器,你可以直接使用。

我们列举了最重要的一些,演示区别。

所用的文本基于:

"Set the shape to semi-transparent by calling set_trans(5)"

标准分析器

标准分析器是es默认的分析器,如果文本可能是各种语言,总的来说选择标准分析器是没错的。

它把文本分成单词(由Unicode决定),移除大部分标点符号,

最终,将所有单词小写化,这样,结果如下:

set , the, shape, to, semi, transparent, by, calling, set_trans,

PS:先清洗,再分词,再归一化。

简单analyzer

这个分析器将文本分词的规则是每个字符是不是一个letter,

然后将所有单词小写化,结果如下:

set , the, shape, to, semi, transparent, by, calling, set , trans

空格analyzer

依靠空格来分词,不将单词小写化,

结果如下:

Set, the, shape, to, semi-transparent, by, calling, set_trans( )

语言分析器

特定语言分析器是可用的,可以处理特殊字符。

比如,英语分析器附带很多英语过滤单词集合,这些集合包括没啥意义的单词。

那分析器就会去掉这些单词,可以对英语单词进行提取词干,

这是因为英语分析器知道英语的语法规则。

结果如下:

set , shape, semi, transpar, call, set_tran,

注意: "transparent" , "calling" , and "set_trans" 已经归一化到词根形式。

什么时候用分析器

当我们索引一个文档,它的整个文档被分析成单词,单词用来创建倒排索引。

尽管如下,当我们搜索全文字段,我们也需要将查询字符串进行同样的分析过程。

实际上也就是说,你之前如何对全文做索引的,这个时候也要对查询字符串做同样的归一化处理。这样才可以保证可以查出来数据。

后面说全文搜索,

当你查询一个全文字段,查询将对查询字符串应用同样的分析器来产生一系列分词后的单词。

当你查询一个具体的字段,查询就不会对查询字符串进行分析,仅仅搜索具体的值。

现在你就理解了之前的:

date字段包含了一个具体的值:一个单词 "2014-09-15" .

_all字段是一个全文字段,所以分析器已经把日期分成 个单词: "2014" , "09" and "15" .

当我们在_all字段里搜索 ,有 个结果,因为都包含 这个单词。

GET /_search?q= # results

当我们在_all字段里搜索 - - , 首先会把查询字符串分词为 "2014" , "09"and "15" .

仍然包含了 个tweets,因为都包含 .

GET /_search?q= - - # results !

当我们查询date字段,值为 - - , 搜索会寻找具体的日期,结果只有 个:

GET /_search?q=date: - - # result

当我们查询date字段,值为 ,就找不到文档。

GET /_search?q=date: # results !

测试分析器

如果你是一个新手,有时会很难理解分词的具体原理和存储索引的过程。

(不看源码你永远不可能知道,just read the fuc*ing source code please!!!)

为了更好的理解怎样运行的,你可以使用分析API来看文本如何分析的。

在查询字符串参数里指定你想用哪个分析器,body里指定分析的文本。

GET /_analyze?analyzer=standard

Text to analyze

结果如下:

{

"tokens" : [

{

"token" "text" ,

"start_offset" : ,

"end_offset" ,

"type" "<ALPHANUM>" ,

"position" :

},

{

"token" "to" ,

"start_offset" : ,

"end_offset" ,

"type" "<ALPHANUM>" ,

"position" :

},

{

"token" "analyze" ,

"start_offset" : ,

"end_offset" ,

"type" "<ALPHANUM>" ,

"position" :

}

]

}

这些单词就是真实的被存储在索引里的单词。

position表明单词出现的顺序,

start_offset 和 end_offset 表明 字符在原始文本里的位置。

分析API对于理解es的索引很有用。

指定分析器

当es发现有需要索引的 String 字段,自动认为是全文字符串字段,用标准分析器来分析。

有的时候你可能不想要这个分析器,纳尼?

也许你想采用一个不同的分析器,因为你觉得它更合适。

也许你还希望一个string字段就是一个字段,不需要认为是全文字段,比如说字符串类型的userid和内部身份。

http://my.oschina.net/qiangzigege/blog/265553

索引里的每个文档有一个type,

每一个type有它自己的映射模式,一个映射指定了type里的字段,每个字段的数据类型,并且字段如何被es处理。

一个映射也被用来配置元数据。

我们会后续详细讨论映射。

核心简单字段类型

es支持下列简单的字段类型:

String: string

Whole number: byte, short, integer, long

Floating point: float, double

Boolean: boolean

Date: date

当你索引一个文档,而这个文档包含了一个新的字段,es会动态映射此字段,规则如下:

JSON type: Field type:

Boolean: true or false "boolean"

Whole number: "long"

Floating point: 123.45 "double"

String, valid date: "2014-09-15" "date"

String: "foo bar" "string"

这也就意味着,如果你索引 “ ”,就映射成字符串,而不是long.

尽管如此,如果这个字段已经映射成long,es将尝试按此转换,失败则抛出异常。

查看映射

使用/_mapping来查看已有的映射关系。

比如查看index:gb type:tweet的映射

GET /gb/_mapping/tweet

结果:

{

"gb" : {

"mappings" : {

"tweet" : {

"properties" : {

"date" : {

"type" : "date" ,

"format" : "dateOptionalTime"

},

"name" : {

"type" : "string"

},

"tweet" : {

"type" : "string"

},

"user_id" : {

"type" : "long"

}

}

}

}

}

}

定制字段的映射

字段最重要的属性就是type,

{

"number_of_clicks" :

{

"type" : "integer"

}

}

string类型的字段,默认,认为是全文搜索,

也就是说,值会传递给分析器,搜索之前也会将查询字符串进行全文分词再搜索。

字符串字段最重要的 个映射关系是index和analyser.

index

index属性决定字符串如何被索引,有 个值:

analyzed

先分词,再索引,全文搜索

not_analyzed

索引这个字段,这样可以被搜索,但是直接索引,不分词。

no

不要索引这个字段,这个字段也不会被搜出来。

string字段默认是analyzed.

下面阐述了不分词的做法

{

"tag" : {

"type" "string" ,

"index" "not_analyzed"

}

}

其它简单的字段类型-long, double , date etc ,

也可以接收index参数,不过只可以取值no ,not_analyzed.

analyzer

对于需要分词的string字段,analyzer属性决定使用哪个分词器,默认,使用standard分词器。

但是你可以设置为一个内置的分词器,比如whitespace,simple,english.

{

"tweet" : {

"type" "string" ,

"analyzer" : "english"

}

}

更新映射

你可以在创建索引时指定映射,另外,你可以使用/_mapping来修改映射或者增加映射。

如果一个字段已经在映射里存在,这也许意味着这个字段的数据已经被索引了,如果你想这个字段的映射,

已经索引过的数据就会出错。

我们可以通过增加一个新的字段来修改映射,但是我们不能修改已经存在的字段从分词到不分词。

删除gb索引

DELETE /gb

然后创建一个索引,指定tweet字段使用english分词器。

PUT /gb

{

"mappings" : {

"tweet" : {

"properties" : {

"tweet" : {

"type" "string" ,

"analyzer" : "english"

},

"date" : {

"type" "date"

},

"name" : {

"type" "string"

},

"user_id" : {

"type" "long"

}

}

}

}

}

这就创建了一个带映射关系的索引。

下面修改映射,增加一个新的不分词的字段tag到tweet映射中。

PUT /gb/_mapping/tweet

{

"properties" : {

"tag" : {

"type" "string" ,

"index" "not_analyzed"

}

}

}

测试映射

可以使用分词API来测试映射,

GET /gb/_analyze?field=tweet

Black-cats

GET /gb/_analyze?field=tag

Black-cats

前者 个分词,后者一个分词。

http://my.oschina.net/qiangzigege/blog/269856

除了简单的数据类型,JSON还有null值,数组和对象,es都支持。

多值字段:

有可能我们想我们的标签字段包含好几个标签,可以用数组:

{ "tag": [ "search", "nosql" ]}

这没有什么特别的,任何字段可以包含0,1或者多个值,这跟一个全文字段产生多个分词道理是一样的。

这意味着,一个数组的所有值必须是同样的数据类型,你不能混杂两种数据类型,

如果你通过索引一个数组而创建了一个新的字段,es将使用第一个值的数据类型来决定整个字段的数据类型。

数组的元素没有顺序,你不能说第一个元素和最后一个元素,就是一个集合。

空字段

数组,可以为空,事实上,Lucene是没有办法存储空值的,所以,一个没有值的字段被认为是一个空的字段。

以下四种字段可以认为是空的,不会被索引:

"empty_string": "",

"null_value": null,

"empty_array": [],

"array_with_null_value": [ null ]

多层对象

最后一个JSON数据类型是对象object,比如哈希,字典和数组。

数据对象嵌套是常见的,比如:

{

"tweet": "Elasticsearch is very flexible",

"user": {

"id": "@johnsmith",

"gender": "male",

"age": 26,

"name": {

"full": "John Smith",

"first": "John",

"last": "Smith"

}

}

}

内部对象的映射

es会探测到新的对象字段并且映射为object类型,

映射如下:

{

"gb": {

"tweet": {

"properties": {

"tweet": { "type": "string" },

"user": {

"type": "object",

"properties": {

"id": { "type": "string" },

"gender": { "type": "string" },

"age": { "type": "long" },

"name": {

"type": "object",

"properties": {

"full": { "type": "string" },

"first": { "type": "string" },

"last": { "type": "string" }

}

}

}

}

}

}

}

}

内部对象如何被索引?

Lucene不知道内部对象,一个Lucene文档包含一个平级的k/v结构。

为了让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]

}

Lucene仅仅索引简单的值,不是复杂的结构。

内部对象数组

最后,思考下,一个内部对象数组如何被索引,

比如说如下:

{

"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} 已经丢失了

Is there a follower who is 26 years old?

这个可以回答

Is there a follower who is 26 years old and who is called Alex Jones?

这个问题就无法回答。

后续再讨论这个问题。http://my.oschina.net/qiangzigege/blog/270948


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多