1、索引文档设计
设计原理与跟数据库设计类似, 主要是文档类型(表)以及其字段的设计。关于数据库系统原理与设计没有系统了解的同学可以通过购买相关书籍,如:江西财大信管学院编写的《数据库系统原理与设计》。为方便理解以及考虑到elasticsearch文档字段的多少对文档的搜索影响不像关系数据库那么大。 这里不考虑索引关联设计与查询(即nested设计类型以及has_child、has_parent查询)只以单文档类型设计为例进行讲解。
考虑到前面已经对相关概念做了介绍,因此直接从我做的项目的缩减版为例开始。
以下是对某产品做的索引,文档类型设计如下:
"mappings" : {
"products_type" : {
"properties" : {
"add_date" : {
"type" : "date",
"format" : "strict_date_optional_time||epoch_millis"
},
"category" : {
"properties" : {
"id" : {
"type" : "string",
"fields" : {
"raw" : {
"type" : "string",
"index" : "not_analyzed"
}
}
},
"name" : {
"type" : "string",
"fields" : {
"raw" : {
"type" : "string",
"index" : "not_analyzed"
}
}
}
}
},
"material" : {
"properties" : {
"id" : {
"type" : "string"
},
"name" : {
"type" : "string",
"fields" : {
"raw" : {
"type" : "string",
"index" : "not_analyzed"
}
}
}
}
},
"color" : {
"properties" : {
"id" : {
"type" : "string"
},
"name" : {
"type" : "string",
"fields" : {
"raw" : {
"type" : "string",
"index" : "not_analyzed"
}
}
}
}
},
"popularity" : {
"type" : "long"
},
"price" : {
"type" : "double"
},
"name" : {
"type" : "string",
"analyzer" : "synonym"
},
"size" : {
"type" : "string",
"fields" : {
"raw" : {
"type" : "string",
"index" : "not_analyzed"
}
}
},
"sku" : {
"type" : "string"
}
}
}
},
"settings" : {
"index" : {
"analysis" : {
"filter" : {
"local_synonym" : {
"type" : "synonym",
"synonyms_path" : "/usr/share/elasticsearch/config/analysis/synonym.txt"
}
},
"analyzer" : {
"synonym" : {
"filter" : [ "lowercase", "local_synonym" ],
"type" : "custom",
"tokenizer" : "standard"
}
}
},
"number_of_shards" : "5",
"number_of_replicas" : "1",
}
}
从mappings 可以看出该产品有以下几个字段:上架日期、分类(id和名称)、材料、颜色、热度、价格、名称、大小、sku码。
进一步会发现有些字段如:分类名称、颜色等, 用fields多配置了一个not_analyzed的raw字段。顾名思义就是额外创建一个未被分析(即与插入的数据一模一样)的字段,此字段对聚合操作有很大用处。
从settings可以看出我们还配置了一个同义词分析器:local_synonym, 而且还可以发现分析器由过滤器(filter)和分词器(tokenizer)组成,另外过滤器也叫语言处理组件(Linguistic Processor)。
这里在补充下elasticsearch对string字段创建索引(即倒排索引)的方法:
当一个string字段的值传入elasticsearch时,值会在分析器的分词器和过滤器处理下转换为一个个Term传到索引组件,索引组件会对得到的所有item创建一个按字母排序字典后进行合并最后生成文档倒排(Posting List) 链表。在倒排链表中会记录以下信息:一个term存在哪几个文档中以及某个文档出现过此term多少次及出现的位置。
二、查询结构体
这里只介绍一个尽可能包含多查询情景下的例子:查找出搜索关键词匹配到分类同时匹配到材料、颜色其中之一的结果与搜索关键词全部在产品标题中的结果的并集,并且返回的结果中要价格最大值和最小值并且返回的结果按公式:0.001*popularity(值上限为3)+_score计算出来的值由大到小排序。
分析:
此需求应分两步:
1、获取所有分类、材料、颜色,可用聚合操作。
2、两种完全不同搜索逻辑的并集:可用dis_max。
实现:
为方便理解, 这里取出一个产品做例子:
{
"_index" : "products",
"_type" : "products_type",
"_id" : "936934",
"_version" : 11,
"found" : true,
"_source" : {
"sku" : "SKU148006",
"name" : "Thin Belt With Small Buckle",
"add_date" : "2017-01-18T00:00:00.000+08:00",
"price" : 7.95,
"popularity" : 0,
"category" : {
"id" : "3264 3097 3169",
"name" : "WOMEN,ACCESSORIES,Belts"
},
"color" : {
"id" : 1234,
"name" : "red"
},
"size" : 2234,
"material" : {
"id" : 3234,
"name" : "cotton"
}
注:这里的分类是三级同时存储,这样既可以保证搜索方便,又不影响分类聚合出来所有结果本身的分类层次。
1、获取所有分类、材料、颜色
from elasticsearch_dsl.connections import connections
# 从配置文件中获取host和端口
es_host_str = ":".join((settings.ES_HOST, str(settings.ES_PORT)))
# 创建es连接
connections.create_connection(hosts=[es_host_str, ])
# 创建聚合dsl,此处有两个size为0, 外面的size为0 是为了只获取聚合到的数据,不用获取具体产品从而加快时间。里面的size为0是为了返回所有的聚合结果,因为es对每项返回的聚合结果数量会有一个默认值(这里是个坑,亲踩,官网未见说明)。
aggs_dsl = {'aggs': {'category': {'terms': {'field':
'category.name.raw', 'size': 0}}, 'color': {'terms': {'field': 'color.name.raw', 'size': 0}}, 'material': {'terms': {'field': 'material.name.raw', 'size': 0}},
'min_price': {'min': {'field': 'price'}}, 'max_price': {'max': {'field': 'price'}}, 'size': 0}
# 发送聚合请求并得到结果
result = connections.get_connection().search(index='products', doc_type='products_type',
body=aggs_dsl , **kwargs)
查询产品
这一步是建立在第一步的基础上的,代码如下:
def get_search_filters_name(keys, syn_words_dict, aggs_results):
"""
匹配产品分类和其他筛选项
key:string类型,为搜索关键字,
syn_words_dict:dict类型,为同义词
"""
syn_words = set(syn_words_dict.keys())
_keys_list_set = set(keys.strip().split(" "))
_keys_list_set.discard("")
keys_set = _keys_list_set | syn_words
aggs_data = aggs_results['aggregations']
fdict = {"bool": {"should": [],
"minimum_should_match": 1}}
name_match_dict = dict(multi_match=dict(
query='',
type='phrase',
fields=["name",]))
ctg_fdict = copy.deepcopy(fdict)
for field, fvalue in aggs_data.iteritems():
_field = ".".join((field, "name"))
tmp = {"match": {_field: {"query": "",
"operator": "and",
"boost": 200,
# "type": "phrase"
}}}
tmp_query_list = []
tmp_im_val_len_list = []
for item in fvalue['buckets']:
item_values = item["key"].lower().split(",")
for level, item_value in enumerate(item_values):
if not item_value:
continue
for rm_and_item in item_value.split("&"):
item_val_set = set([rai.replace("-", ' ') \
for rai in rm_and_item.strip().split(" ")])
item_val_set -= exclude_words
if field == 'category':
tmp_im_val_len_list.append((item_val_set, level))
else:
tmp_im_val_len_list.append((item_val_set, len(item_val_set)))
# 将分类与其他区分开,是因为二者的匹配顺序不同
# 分类是按分类层次有上至下匹配,而其他是按单词个数由多到少顺序匹配
if field == 'category':
tmp_im_val_len_list = sorted(tmp_im_val_len_list, key=lambda x: x[-1]
)
else:
tmp_im_val_len_list = sorted(tmp_im_val_len_list, key=lambda x: x[-1],
reverse=True)
for s_item_val_set, set_len in tmp_im_val_len_list:
set_len = len(s_item_val_set)
f_syn_set = set()
is_syn_cate = False
if s_item_val_set and not (s_item_val_set - keys_set):
for iv in s_item_val_set:
if iv in syn_words:
syn_set = syn_words_dict[iv]
# 防止匹配到的分类词中有两个或者以上单词是来自同一组近义词
if set_len > 1 and not len(s_item_val_set - syn_set) < set_len -1:
is_syn_cate = True
break
f_syn_set |= syn_set
if not is_syn_cate:
keys_set -= s_item_val_set
tmp_query_list.append(" ".join(s_item_val_set))
if tmp_query_list:
if field == "category":
for tmp_q_l in tmp_query_list:
tmp_cp = copy.deepcopy(tmp)
tmp_cp["match"][_field]["query"] = tmp_q_l
ctg_fdict["bool"]["should"].append(tmp_cp)
else:
for tmp_q_l in tmp_query_list:
tmp_cp = copy.deepcopy(tmp)
tmp_cp["match"][_field]["query"] = tmp_q_l
fdict["bool"]["should"].append(tmp_cp)
final_keys = keys_set & raw_keys_set
no_filter_key = " ".join(final_keys).strip()
# 最终返回没有匹配到筛选项和分类的剩下的词,匹配到的分类过滤dsl, 筛选项的过滤dsl
return no_filter_key, ctg_fdict, fdict
这里再把没有匹配到筛选项和分类的剩下的词再去匹配标题可以用来提升匹配得分,因此比如搜索red mini dress的最终的查询dsl为:
{'sort': ['_score', {'popularity': {'order': 'desc'}}],
'query': {'function_score': {'query': {'bool': {'must': [{'dis_max': {'queries': [
{'bool': {'must': [
{'bool': {'minimum_should_match': 1, 'should': [{'match': {u'color.name': {'operator': 'and', 'query': u'red'}}},
{'multi_match': {'fields': ['name',], 'query': u'red'}}]}},
{'bool': {'minimum_should_match': 1, 'should': [{'match': {u'category.name': {'operator': 'and', 'query': u'dress'}}},
{'multi_match': {'fields': ['name',], 'query': u'dress'}}]}},
{'bool': {'minimum_should_match': 1, 'should': [{'multi_match': {'fields': ['name'], 'query': u'mini'}}]}}]}},
{'bool': {'must':[{'bool': {'minimum_should_match': 1, 'boost': 100, 'should': [{'multi_match': {'fields': ['name^4',], 'query': u'red mini dress'}}]}}]}}]}}]}},
'max_boost': 2, 'field_value_factor': {'field': 'popularity', 'factor': 0.001, 'missing': 0}, 'boost_mode': 'sum', "max_boost": 3}},
'from': 0, 'size': 36}
注:聚合操作是搜索引擎的一个非常大的强项,除了可以得到所有不同的结果外,聚合操作还可以同时得到相应结果的数量。
Copyright © 2021.aoyanming个人博客站
发表评论