基本情况就是,媒体、试题、分类,媒体可能有多个试题,一个试题可能有多个分类,分类为三级分类加上一个综合属性。通过试题名称、分类等搜索查询媒体。
现在的问题为,搜索结果不精确,部分搜索无结果,ES的数据结构不满足搜索需求。解决方案就是,重构ES数据结构,采用父子关系的方式,建立media和question两个type。
全程使用https://github.com/mobz/elasticsearch-head,这个进行ES的管理和查看,很方便。
从ES的说明可以看出,ES是面向文档,其实所有的数据都是一张张卡片,例如下面这个:
几个重要的概念:mapping,index,type可以直接参考上图:
_index,可以看做是数据库吧,上图命名为links,在对搜索进行操作的时候需要指定,就像指定数据库一样。
_type,约等于表,例如这里的media,上图的_type列大家还可以看到有question数值。其实就等于我们的media表和question表
mapping,映射、绘制...的地图,顾名思义。其实就是表结构和表关系。例如上面点开的卡片内,_source内有id,language等,其实就是mapping。mapping还包括关系的定义,例如这里的media是parent父级,question的结构创建的时候就需要指定_parent为media。
了解了以上几个概念,我们就可以进行结构创建了。就像数据库一样,我们需要一个media表,放媒体信息,媒体ID作为唯一的ID。然后question表,放question的信息(这里还包括试题的分类),我们把同一个试题分配为不同分类也算作不同试题。这里这样的结构,也是为了根据多级分类搜索的时候方便而设置的,下面说搜索的时候会挑明。
这是初始化创建index和mapping的代码:
$elasticaClient = new \Elastica\Client(array('host'=>'localhost','port'=>9200)); // Load index $elasticaIndex = $elasticaClient->getIndex('links'); // Create the index new // 创建index的参数自行参见官网,就不一一解释了 $elasticaIndex->create( array( 'number_of_shards' => 4, 'number_of_replicas' => 1, 'analysis' => array( 'analyzer' => array( 'indexAnalyzer' => array( 'type' => 'custom', 'tokenizer' => 'standard', 'filter' => array('lowercase', 'mySnowball') ), 'searchAnalyzer' => array( 'type' => 'custom', 'tokenizer' => 'standard', 'filter' => array('standard', 'lowercase', 'mySnowball') ) ), 'filter' => array( 'mySnowball' => array( 'type' => 'snowball', 'language' => 'German' ) ) ) ), true ); //创建media的mapping,作为父级 $mediaType = $elasticaIndex->getType('media'); // Define mapping $mapping = new \Elastica\Type\Mapping(); $mapping->setType($mediaType); $mapping->setParam('index_analyzer', 'indexAnalyzer'); $mapping->setParam('search_analyzer', 'searchAnalyzer'); // Define boost field $mapping->setParam('_boost', array('name' => '_boost', 'null_value' => 1.0)); // Set mapping // 定义media的字段和属性 $mapping->setProperties(array( 'id' => array('type' => 'string', 'include_in_all' => FALSE), 'media_name' => array('type' => 'string', 'include_in_all' => TRUE), 'tstamp' => array('type' => 'date', 'include_in_all' => FALSE), 'language' => array('type' => 'integer', 'include_in_all' => FALSE), '_boost' => array('type' => 'float', 'include_in_all' => FALSE) )); // Send mapping to type // 保存media的mapping $mapping->send(); //创建question的mapping,父级为media $questionType = $elasticaIndex->getType('question'); // Define mapping $mapping = new \Elastica\Type\Mapping(); $mapping->setType($questionType); $mapping->setParam('index_analyzer', 'indexAnalyzer'); $mapping->setParam('search_analyzer', 'searchAnalyzer'); // Define boost field $mapping->setParam('_boost', array('name' => '_boost', 'null_value' => 1.0)); // Set mapping // question的字段和属性 $mapping->setProperties(array( 'id' => array('type' => 'string', 'include_in_all' => FALSE), 'level_one' => array('type' => 'integer', 'include_in_all' => FALSE), 'level_two' => array('type' => 'integer', 'include_in_all' => FALSE), 'level_thr' => array('type' => 'integer', 'include_in_all' => FALSE), 'top_level' => array('type' => 'string', 'include_in_all' => FALSE), 'cat_id' => array('type' => 'integer', 'include_in_all' => FALSE), 'quest_hash' => array('type' => 'string', 'include_in_all' => TRUE), 'content' => array('type' => 'string', 'include_in_all' => TRUE), 'view_num' => array('type' => 'integer', 'include_in_all' => FALSE), 'like_num' => array('type' => 'integer', 'include_in_all' => FALSE), '_boost' => array('type' => 'float', 'include_in_all' => FALSE) )); $mapping->setParent("media");//指定question的父类 // Send mapping to type // 保存question的mapping $mapping->send();
上面虽然是PHP代码,但是最终生成的也是一个url请求。
下面说搜索,搜索的话ES是通过query、filter等来处理的,query里面有很多不同的方式,参见:http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-queries.html,filter也是,参见http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-filters.html
这里搜索是这样的,根据media的media_name做query_string搜索,然后对media进行has_child的filter搜索,has_child搜索内使用boolAnd的filter来筛选。
下面是搜索的代码:
$query = new \Elastica\Query(); if (!empty($input['key'])) { //针对media的media_name字段设置QueryString查询 $elasticaQueryString = new \Elastica\Query\QueryString(); $elasticaQueryString->setFields(array("media.media_name")); $elasticaQueryString->setQuery($input['key']); // $query->setQuery($elasticaQueryString); }else { $query->setQuery(new MatchAll()); //命中全部纪录 } $language_bool = false; $elasticaFilterAnd = new \Elastica\Filter\BoolAnd(); //language也是针对media,设置BoolAnd查询 if (isset($input['language']) && !empty($input['language'])) { $filterl1 = new \Elastica\Filter\Term(); $filterl1->setTerm('language', intval($input['language'])); $elasticaFilterAnd->addFilter($filterl1); $language_bool = true; } // //对子集进行筛选查询,使用has_child $subFilterAnd = new \Elastica\Filter\BoolAnd(); $bool = false; // 一级分类条件 if (isset($input['level_one']) && !empty($input['level_one'])) { $filterl1 = new \Elastica\Filter\Term(); $filterl1->setTerm('level_one', intval($input['level_one'])); $subFilterAnd->addFilter($filterl1); $bool = true; } // 二级分类条件 if (isset($input['level_two']) && !empty($input['level_two'])) { $filterl1 = new \Elastica\Filter\Term(); $filterl1->setTerm('level_two', intval($input['level_two'])); $subFilterAnd->addFilter($filterl1); $bool = true; } // 三级分类条件 if (isset($input['level_thr']) && !empty($input['level_thr'])) { $filterl1 = new \Elastica\Filter\Term(); $filterl1->setTerm('level_thr', intval($input['level_thr'])); $subFilterAnd->addFilter($filterl1); $bool = true; } // 直接指定分类ID查询 if (isset($input['cat_id']) && !empty($input['cat_id'])) { $filterl1 = new \Elastica\Filter\Term(); $filterl1->setTerm('cat_id', intval($input['cat_id'])); $subFilterAnd->addFilter($filterl1); $bool = true; } // 分类属性查询 if (isset($input['top_level']) && !empty($input['top_level'])) { $filterl1 = new \Elastica\Filter\Term(); $filterl1->setTerm('top_level', $input['top_level']); $subFilterAnd->addFilter($filterl1); $bool = true; } if($bool){ // 声明一个查询,用于放入子查询 $subQuery = new \Elastica\Query(); // 使用filteredquery,融合query和filter $filteredQuery = new \Elastica\Query\Filtered(new MatchAll(),$subFilterAnd); // 添加filterquery到子查询 $subQuery->setQuery($filteredQuery); // 声明hasChildFilter,声明的时候就指定子查询的内容,指定查询的子表(也就是TYPE)为question $filterHasChild = new \Elastica\Filter\HasChild($subQuery,"question"); // 将拥有子类查询增加到父级查询的filter中 $elasticaFilterAnd->addFilter($filterHasChild); } if($bool || $language_bool){ // 将filter增加到父查询汇中 $query->setFilter($elasticaFilterAnd); } // // $query->setFrom($start); // Where to start? $query->setLimit($limit); // How many? // //Search on the index. $elasticaResultSet = $elasticaIndex->search($query);
上面看上去很长的PHP代码,其实最后发出的时候也只是一个发送json数据的请求,对照下面这个json数据和上面的代码,大家就很容易明白了:
{ "query": { "query_string": { "query": "like", "fields": [ "media.media_name" ] } }, "filter": { "and": [ { "term": { "language": 1 } }, { "has_child": { "query": { "filtered": { "query": { "match_all": {} }, "filter": { "and": [ { "term": { "top_level": "111" } } ] } } }, "type": "question" } } ] }, "from": 0, "size": 20 }
总结:ES很强大,不仅仅是在导入性能还是搜索性能,或者是搜索结果,或者是结构的调整上来说。作为刚接触不久的也能很快的进行数据结构重构并重写搜索,还是算比较好的。唯一的缺点就是,中文的文档太少,需要不断的使用谷歌来查看文档、去官网看文档说明、看PHP的API。
在处理过程中,感谢之前的哥们留下的代码,至少不是摸瞎。感谢同事搞的谷歌搜索,一般人我不告诉他,大家可以去试试 另客网,首页的搜索框里面,选择谷歌。
部分借鉴来自于这里:http://www.spacevatican.org/2012/6/3/fun-with-elasticsearch-s-children-and-nested-documents/
个人笔记,写来大家分享分享,肯定有不足错误的,欢迎大家指出,谢谢。