網站基于ElasticSearch搜索的優化筆記 PHP
基本情況就是,媒體、試題、分類,媒體可能有多個試題,一個試題可能有多個分類,分類為三級分類加上一個綜合屬性。通過試題名稱、分類等搜索查詢媒體。
現在的問題為,搜索結果不精確,部分搜索無結果,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/
來自:http://my.oschina.net/u/730537/blog/288703