网站基于ElasticSearch搜索的优化笔记 PHP

jopen 10年前

基本情况就是,媒体、试题、分类,媒体可能有多个试题,一个试题可能有多个分类,分类为三级分类加上一个综合属性。通过试题名称、分类等搜索查询媒体。

现在的问题为,搜索结果不精确,部分搜索无结果,ES的数据结构不满足搜索需求。解决方案就是,重构ES数据结构,采用父子关系的方式,建立media和question两个type。

全程使用https://github.com/mobz/elasticsearch-head,这个进行ES的管理和查看,很方便。

从ES的说明可以看出,ES是面向文档,其实所有的数据都是一张张卡片,例如下面这个:

151203_bqdp_730537.png

几个重要的概念: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/

来自:http://my.oschina.net/u/730537/blog/288703