虽然ES自己已经提供了一些解析器,可是经过组合字符过滤器(Character Filter),分词器(Tokenizer)以及词条过滤器(Token Filter)来建立你本身的解析器才会显示出其威力。html
在解析和解析器中,咱们提到过解析器(Analyzer)就是将3种功能打包获得的,它会按照下面的顺序执行:node
字符过滤器(Character Filter) 字符过滤器用来在分词前将字符串进行"整理"。好比,若是文本是HTML格式,那么它会含有相似<p>或者<div>这样的HTML标签,可是这些标签咱们是不须要索引的。咱们可使用html_strip字符过滤器移除全部的HTML标签,并将全部的像Á这样的HTML实体(HTML Entity)转换为对应的Unicode字符:Á。mysql
分词器(Tokenizers) 一个解析器必须有一个分词器。分词器将字符串分解成一个个单独的词条(Term or Token)。在standard解析器中使用的standard分词器,经过单词边界对字符串进行划分来获得词条,同时会移除大部分的标点符号。另外还有其余的分词器拥有着不一样的行为。正则表达式
好比keyword
分词器,它不会进行任何分词,直接原样输出。whitespace
分词器则只经过对空白字符进行划分来获得词条。而pattern
分词器则根据正则表达式来进行分词。sql
词条过滤器(Token Filter) 在分词后,获得的词条流(Token Stream)会按照顺序被传入到指定的词条过滤器中。数据库
词条过滤器可以修改,增长或者删除词条。咱们已经提到了lowercase
词条过滤器和stop
词条过滤器,可是ES中还有许多其它可用的词条过滤器。stemming
词条过滤器会对单词进行词干提取来获得其词根形态(Root Form)。ascii_folding
词条过滤器则会移除变音符号(Diacritics),将相似于très
的词条转换成tres
。ngram
词条过滤器和edge_ngram
词条过滤器会产生适用于部分匹配(Partial Matching)或者自动完成(Autocomplete)的词条。json
在深刻搜索中,咱们会经过例子来讨论这些分词器和过滤器的使用场景和使用方法。可是首先,咱们须要解释如何来建立一个自定义的解析器。app
在深刻搜索中,咱们会经过例子来讨论这些分词器和过滤器的使用场景和使用方法。可是首先,咱们须要解释如何来建立一个自定义的解析器。elasticsearch
和上面咱们配置es_std
解析器的方式相同,咱们能够在analysis
下对字符过滤器,分词器和词条过滤器进行配置:ide
PUT /my_index { "settings": { "analysis": { "char_filter": { ... custom character filters ... }, "tokenizer": { ... custom tokenizers ... }, "filter": { ... custom token filters ... }, "analyzer": { ... custom analyzers ... } } } }
好比,要建立拥有以下功能的解析器:
html_strip
字符过滤器完成HTML标签的移除。mapping
字符过滤器。"char_filter": { "&_to_and": { "type": "mapping", "mappings": [ "&=> and "] } }
standard
分词器对文本进行分词。lowercase
词条过滤器将全部词条转换为小写。"filter": { "my_stopwords": { "type": "stop", "stopwords": [ "the", "a" ] } }
咱们的解析器将预先定义的分词器和过滤器和自定义的过滤器进行告终合:
"analyzer": { "my_analyzer": { "type": "custom", "char_filter": [ "html_strip", "&_to_and" ], "tokenizer": "standard", "filter": [ "lowercase", "my_stopwords" ] } }
所以,整个create-index
请求就像下面这样
PUT /my_index { "settings": { "analysis": { "char_filter": { "&_to_and": { "type": "mapping", "mappings": [ "&=> and "] }}, "filter": { "my_stopwords": { "type": "stop", "stopwords": [ "the", "a" ] }}, "analyzer": { "my_analyzer": { "type": "custom", "char_filter": [ "html_strip", "&_to_and" ], "tokenizer": "standard", "filter": [ "lowercase", "my_stopwords" ] }} }}}
建立索引以后,使用analyze
API对新的解析器进行测试:
http://node1:9200/my_index/_analyze?analyzer=my_analyzer&text="The Quick brown fox" { "tokens": [ { "token": "quick", "start_offset": 5, "end_offset": 10, "type": "<ALPHANUM>", "position": 1 }, { "token": "brown", "start_offset": 13, "end_offset": 18, "type": "<ALPHANUM>", "position": 2 }, { "token": "fox", "start_offset": 19, "end_offset": 22, "type": "<ALPHANUM>", "position": 3 } ] }
咱们须要告诉ES这个解析器应该在什么地方使用。咱们能够将它应用在text字段的映射中:
PUT /my_index/_mapping/my_type { "properties": { "title": { "type": "text", "analyzer": "my_analyzer" } } }
在ES中的类型(Type)表明的是一类类似的文档。一个类型包含了一个名字(Name) - 好比user
或者blogpost
- 以及一个映射(Mapping)。映射就像数据库的模式那样,描述了文档中的字段或者属性,和每一个字段的数据类型 -text,integer
,date
等 - 这些字段是如何被Lucene索引和存储的。
在什么是文档中,咱们说一个类型就比如关系数据库中的一张表。尽管一开始这样思考有助于理解,可是对类型自己进行更细致的解释 - 它们究竟是什么,它们是如何在Lucene的基础之上实现的 - 仍然是有价值的。
Lucene中的文档包含的是一个简单field-value对的列表。一个字段至少要有一个值,可是任何字段均可以拥有多个值。相似的,一个字符串值也能够经过解析阶段而被转换为多个值。Lucene无论值是字符串类型,仍是数值类型或者什么别的类型 - 全部的值都会被同等看作一些不透明的字节(Opaque bytes)。
当咱们使用Lucene对文档进行索引时,每一个字段的值都会被添加到倒排索引(Inverted Index)的对应字段中。原始值也能够被选择是否会不做修改的被保存到索引中,以此来方便未来的获取。
ES中的type是基于如下简单的基础进行实现的。一个索引中能够有若干个类型,每一个类型又有它本身的mapping,而后类型下的任何文档能够存储在同一个索引中。
但是Lucene中并无文档类型这一律念。因此在具体实现中,类型信息经过一个元数据字段_type
记录在文档中。当咱们须要搜索某个特定类型的文档时,ES会自动地加上一个针对_type字段的过滤器来保证返回的结果都是目标类型上的文档。
同时,Lucene中也没有映射的概念。映射是ES为了对复杂JSON文档进行扁平化(能够被Lucene索引)而设计的一个中间层。
好比,user类型的name字段能够定义成一个string类型的字段,而它的值则应该被whitespace解析器进行解析,而后再被索引到名为name的倒排索引中。
"name": { "type": "string", "analyzer": "whitespace" }
因为不一样类型的文档可以被添加到相同的索引中,产生了一些意想不到的问题。
好比在咱们的索引中,存在两个类型:blog_en用来保存英文的博文,blog_es用来保存西班牙文的博文。这两种类型中都有一个title字段,只不过它们使用的解析器分别是english和spanish。
问题能够经过下面的查询反映:
GET /_search { "query": { "match": { "title": "The quick brown fox" } } }
咱们在两个类型中搜索title字段。查询字符串(Query String)须要被解析,可是应该使用哪一个解析器:是spanish仍是english?答案是会利用首先找到的title字段对应的解析器,所以对于部分文档这样作是正确的,对于另外一部分则否则。
咱们能够经过将字段命名地不一样 - 好比title_en和title_es - 或者经过显式地将类型名包含在字段名中,而后利用multi_match对每一个字段独立查询来避免这个问题:
GET /_search { "query": { "multi_match": { "query": "The quick brown fox", "fields": [ "blog_en.title", "blog_es.title" ] } } }
multi_match查询会对指定的多个字段运行match查询,而后合并它们的结果。
以上的查询中对blog_en.title字段使用english解析器,对blog_es.title字段使用spanish解析器,而后对两个字段的搜索结果按照相关度分值进行合并。
这个解决方案可以在两个域是相同数据类型时起做用,可是考虑下面的场景,当向相同索引中添加两份文档时会发生什么:
类型user
{ "login": "john_smith" }
类型event
{ "login": "2014-06-01" }
Lucene自己不在乎类型一个字段是字符串类型,而另外一个字段是日期类型 - 它只是愉快地将它们当作字节数据进行索引。
可是当咱们试图去针对event.login字段进行排序的时候,ES须要将login字段的值读入到内存中。根据Fielddata提到的,ES会将索引中的全部文档都读入,不管其类型是什么。
取决于ES首先发现的login字段的类型,它会试图将这些值当作字符串或者日期类型读入。所以,这会产生意料外的结果或者直接失败。
Tip 为了不发生这些冲突,建议索引中,每一个类型的同名字段都使用相同的映射方式。