網站基於ElasticSearch搜索的優化筆記 PHP

基本狀況就是,媒體、試題、分類,媒體可能有多個試題,一個試題可能有多個分類,分類爲三級分類加上一個綜合屬性。經過試題名稱、分類等搜索查詢媒體。php

如今的問題爲,搜索結果不精確,部分搜索無結果,ES的數據結構不知足搜索需求。解決方案就是,重構ES數據結構,採用父子關係的方式,創建media和question兩個type。html

全程使用https://github.com/mobz/elasticsearch-head,這個進行ES的管理和查看,很方便。git

從ES的說明能夠看出,ES是面向文檔,其實全部的數據都是一張張卡片,例以下面這個:github

幾個重要的概念:mapping,index,type能夠直接參考上圖:
_index,能夠看作是數據庫吧,上圖命名爲links,在對搜索進行操做的時候須要指定,就像指定數據庫同樣。web

_type,約等於表,例如這裏的media,上圖的_type列你們還能夠看到有question數值。其實就等於咱們的media表和question表數據庫

mapping,映射、繪製...的地圖,顧名思義。其實就是表結構和表關係。例如上面點開的卡片內,_source內有id,language等,其實就是mapping。mapping還包括關係的定義,例如這裏的media是parent父級,question的結構建立的時候就須要指定_parent爲media。json

瞭解了以上幾個概念,咱們就能夠進行結構建立了。就像數據庫同樣,咱們須要一個media表,放媒體信息,媒體ID做爲惟一的ID。而後question表,放question的信息(這裏還包括試題的分類),咱們把同一個試題分配爲不一樣分類也算做不一樣試題。這裏這樣的結構,也是爲了根據多級分類搜索的時候方便而設置的,下面說搜索的時候會挑明。數據結構

這是初始化建立index和mapping的代碼:app

    $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請求。elasticsearch


下面說搜索,搜索的話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/

我的筆記,寫來你們分享分享,確定有不足錯誤的,歡迎你們指出,謝謝。

相關文章
相關標籤/搜索