搜索功能


Couchcms的搜索机制是采用MySQL 的全文搜索功能。

这样做的好处是,Couch 可以为页面中不同位置出现的单词分配不同的权重,并根据计算出的相关性按顺序获取页面。

因此,标题中包含搜索词的页面将被认为比可编辑区域中包含相同词的页面更相关。同样,多次包含搜索词的页面也被认为比仅包含一次的页面更相关。

全文搜索也有一些需要注意的缺点——它不能用于搜索少于四个字符的单词(因为这些单词不重要)。此外,全文搜索无法匹配部分单词。

给定一个单词或几个单词,Couch 支持通过模板的所有定义的可编辑区域(以及一些系统字段 - 即标题名称字段)来搜索它们,并返回包含这些单词的所有页面。

Couch 用来查找包含搜索词的页面的标签是search标签。该标签与之前讨论过的pages标签非常相似,都是先获取相关页面,然后逐一遍历,并设置描述当前页面的变量。

此标签支持两个参数:masterpagelimit。默认情况下,搜索标签会搜索所有可用模板的页面。如果您希望仅搜索网站的某个部分,请使用masterpage参数,让 Couch 仅搜索特定模板或排除某些模板。

可以设置Limit参数,仅显示找到的有限数量的页面。其余页面可以分页显示(参见分页)。

如上所述,此标记会遍历所有找到的页面,并在遍历过程中设置与每个页面相关的变量。因此,以下代码片段 -

<cms:search masterpage='blog.php' limit='10' >
    <h3><a href="<cms:show k_page_link />"><cms:show k_search_title /></a></h3>
    <cms:show k_search_excerpt />
</cms:search>

- 将获取符合当前搜索的前 10 个博客页面。

对于每个页面,其页面视图中通常可用的所有变量都将可用。除此之外,以下变量也可用:

  • k_search_title
  • k_search_content
  • k_search_excerpt

k_search_content是被搜索页面的全部内容,而k_search_excerpt则包含页面中每个搜索词出现的部分,并由这些部分的简短摘录组成。通常,您会将k_search_excerpt显示为搜索结果,因为它还具有高亮显示所有搜索词的附加功能。k_search_title 也会高亮显示搜索词(如果存在),因此比常规的k_page_title变量更适合使用。

这就引出了一个问题——上面到底搜索的是什么?

有两种不同的方法可以向搜索标签指示这一点

1. 搜索标签被设计为从调用该标签的页面的查询字符串(URL 中“?”之后的部分)中名为“s”的参数获取搜索词。在上面给出的代码片段中,搜索标签期望通过查询字符串传递搜索词。因此,如果使用以下 URL 调用放置搜索代码片段的页面,搜索标签将搜索两个词:“hello”和“world”  http ://www.yoursite.com/search.php ?s=hello+world

2.您可以将搜索标签的“关键词”参数设置为要搜索的词条。此方法允许使用变量来指定搜索词。实际上,我们甚至可以从 URL 的查询字符串中获取值,并通过“关键词”参数将其传递给搜索标签-

<cms:search masterpage='blog.php,news.php,product.php' limit='10' keywords="<cms:gpc 's' />" >
    <h3><a href="<cms:show k_page_link />"><cms:show k_search_title /></a></h3>
    <cms:show k_search_excerpt />
</cms:search>

在上面的例子中,我们使用gpc标签从 URL 中获取名为“s”的查询字符串参数,并将其值设置为要搜索的关键字,从而使该示例的行为与没有明确指定任何关键字的搜索标签的行为完全相同。

注意以上的代码可以在多个模块里获取结果

搜索表单

在我们上面提到的为搜索标签指定搜索词的两种不同方法中,通常第一种方法将搜索词设置为查询字符串中名为“s”的参数,在大多数情况下应该足够了。

我们可以使用包含名为“s”的文本字段的 HTML 表单,在查询字符串中设置此参数。提交此表单时,文本字段的任何内容都将自动添加到查询字符串中,并传递给处理提交的页面。

Couch 有一个名为search_form的简单标签,它可以为您生成一个包含名为“s”的文本字段的表单 -

<cms:search_form />

上面的代码片段会生成一个搜索表单,提交后,会在查询字符串中输入搜索词,并调用该代码片段所在的页面。当然,您需要在同一页面上放置一个搜索标签来处理搜索。

您可以创建并使用自己的表单,而不必使用search_form标签创建的表单。只需确保用户输入关键字的文本框名为“s”。

有时,您可能希望在一个页面启动搜索,并在另一个页面显示结果。例如,您可能希望将搜索表单放在网站首页 ( index.php ) 上,但希望用户提交此表单后跳转到另一个页面 ( search.php ),并在该页面显示搜索结果。您可以将search_form标签放在index.php,并将search标签代码片段放在search.php中来实现。要使search_form标签在表单提交时调用search.php(而不是index.php ),可以按以下方式设置处理器参数:

<cms:search_form msg='Enter keywords' processor="<cms:show k_site_link />search.php" />

msg 参数用于在搜索框中显示一些消息。默认文本为“Search”。

作为最后一个例子,以下是可以放置在搜索页面上的代码片段 -

<cms:search_form />
<cms:search limit='10' >
    <cms:if k_paginated_top >
        <div>
            <cms:if k_paginator_required >
                Page <cms:show k_current_page /> of <cms:show k_total_pages /><br>
            </cms:if>
            <cms:show k_total_records /> Pages Found -
            displaying: <cms:show k_record_from />-<cms:show k_record_to />
        </div>
    </cms:if>
    <h3><a href="<cms:show k_page_link />"><cms:show k_search_title /></a></h3>
    <cms:show k_search_excerpt />
    <hr>
    
    <cms:no_results>
        // 如果没有结果,出现这里的内容
    </cms:no_results>

    <cms:paginator />
</cms:search>

附加:

如上所说Couchcms使用mysql全文搜索功能实现的搜索,它不支持4个字符一下的单词(因为mysql认为4个字母以下单词意义不大),我用中文测试后也是不支持4个中文字以下的搜索,这个就很不适合国情。 

我们将做一些改造:

找到couch/addons/kfunctions.php文件(如果此文件不存在,请将kfunctions.example.php重命名为kfunctions.php),将以下代码粘贴进去:

/////////// begin <cms:search_ex> ///////////
    class SearchEx{
        static function tag_handler( $tag_name, $params, $node, &$html ){
            global $CTX, $FUNCS, $TAGS, $DB, $AUTH;

            extract( $FUNCS->get_named_vars(
                       array(
                             'masterpage'=>'',
                             'keywords'=>'',
                            ),
                       $params) );

            $masterpage = trim( $masterpage );
            if( $keywords ){
                $keywords = trim( $keywords );
            }
            else{
                $keywords = trim( $_GET['s'] );
            }
            // is something being searched?
            if( !$keywords ) return;

            // get the keywords being searched
            $keywords = strip_tags( $keywords );
            $orig_keywords = $keywords;
            $keywords = str_replace( array(",", "+", "-", "(", ")"), ' ', $keywords );

            // http://www.askingbox.com/tip/mysql-combine-full-text-search-with-like-search-for-words-with-3-letters
            $searchwords = array_filter( array_map("trim", explode(' ', $keywords)) );
            $longwords = array();
            $shortwords = array();

            //下面是不超过五个字的搜索,如果是2个,3个,4个,5个字都用替代方式
            foreach( $searchwords as $word ){
                if( mb_strlen($word, 'UTF-8')>5 ){
                    $longwords[] = $word;
                }
                else{
                    if( mb_strlen($word, 'UTF-8')==2 || mb_strlen($word, 'UTF-8')==3 || mb_strlen($word, 'UTF-8')==4 || mb_strlen($word, 'UTF-8')==5 ){
                        if(!in_array($val,array('and','the','him','her')))
                        $shortwords[] = $word;
                    }
                }
            }

            if( !count($shortwords) ){ // 如果不是上面指定的字符数量就使用原始的 cms:search 标签搜索
                if( !count($longwords) ) return;

                $keywords = implode (' ', $longwords );

                // delegate to 'pages' tag
                for( $x=0; $x<count($params); $x++ ){
                    $param = &$params[$x];
                    if( strtolower($param['lhs'])=='keywords' ){
                        $param['rhs'] = $keywords;
                        $added = 1;
                        break;
                    }
                }
                if( !$added ){
                    $params[] = array('lhs'=>'keywords', 'op'=>'=', 'rhs'=>$keywords);
                }

                $html = $TAGS->pages( $params, $node, 1 );
            }
            else{
                // we have 3 character words to contend with.

                // craft sql query
                // select ..
                $sql = 'SELECT cp.template_id, cp.id, cf.title, cf.content';

                if( count($longwords) ){
                    // add the '+' for boolean search
                    $sep = "";
                    foreach( $longwords as $kw ){
                        $bool_keywords .= $sep . "+" . $kw;
                        $sep = " ";
                    }

                    $sql .= ", ((MATCH(cf.content) AGAINST ('".$DB->sanitize($bool_keywords)."') * 1) + (MATCH(cf.title) AGAINST ('".$DB->sanitize($bool_keywords)."') * 1.25)) as score";
                }

                // from ..
                $sql .= "\r\n" . 'FROM ';
                $sql .= K_TBL_FULLTEXT ." cf
                inner join  ".K_TBL_PAGES." cp on cp.id=cf.page_id
                inner join ".K_TBL_TEMPLATES." ct on ct.id=cp.template_id";

                // where ..
                $sql .= "\r\n" . 'WHERE ';
                if( count($longwords) ){
                    $sql .= " ((MATCH(cf.content) AGAINST ('".$DB->sanitize($bool_keywords)."' IN BOOLEAN MODE) * 1) + (MATCH(cf.title) AGAINST ('".$DB->sanitize($bool_keywords)."' IN BOOLEAN MODE) * 1.25))";
                }
                else{
                    $sql .= '1=1';
                }
                if( $masterpage ){
                    // masterpage="NOT blog.php, testimonial.php"
                    $sql .= $FUNCS->gen_sql( $masterpage, 'ct.name');
                }
                foreach( $shortwords as $kw ){
                    $sql .= " AND (cf.content LIKE '%".$DB->sanitize($kw)."%' OR cf.title LIKE '%".$DB->sanitize($kw)."%')";
                }
                if( $hide_future_entries ){
                    $sql .= " AND cp.publish_date < '".$FUNCS->get_current_desktop_time()."'";
                }
                $sql .= " AND NOT cp.publish_date = '0000-00-00 00:00:00'";
                $sql .= " AND cp.access_level<='".$AUTH->user->access_level."'";
                $sql .= " AND ct.executable=1";

                // order
                if( count($longwords) ){
                    $sql .= "\r\n";
                    $sql .= "ORDER BY score DESC";
                }

                // delegate sql to cms:query
                for( $x=0; $x<count($params); $x++ ){
                    $param = &$params[$x];
                    if( strtolower($param['lhs'])=='sql' ){
                        $param['rhs'] = $sql;
                        $added = 1;
                        break;
                    }
                }
                if( !$added ){
                    $params[] = array('lhs'=>'sql', 'op'=>'=', 'rhs'=>$sql);
                }
                $params[] = array('lhs'=>'fetch_pages', 'op'=>'=', 'rhs'=>'1');

                $FUNCS->add_event_listener( 'alter_page_tag_context', array('SearchEx', 'ctx_handler') );
                $node->__my_keywords = array_merge( $longwords, $shortwords );
                $html = $TAGS->pages( $params, $node, 5 );
                $FUNCS->remove_event_listener( 'alter_page_tag_context', array('SearchEx', 'ctx_handler') );
            }

            $CTX->set( 'k_search_query', $orig_keywords, 'global' );
            
            return 1; // prevent the original tag from executing as we have set the output above

        }// end func tag_handler

        static function ctx_handler( $rec, $mode, $params, $node ){
            global $CTX, $FUNCS;

            if( !is_array($node->__my_keywords) ) return;

            $keywords = $node->__my_keywords;
            if( $CTX->get('k_template_is_clonable', true) ){
                $hilited = $FUNCS->hilite_search_terms( $keywords, $rec['title'], 1 );
            }
            else{
                $tpl_title = $CTX->get('k_template_title', true);
                $hilited = $tpl_title ? $tpl_title : $CTX->get('k_template_name', true);
            }
            $CTX->set( 'k_search_title', $hilited );
            $CTX->set( 'k_search_content', $rec['content'] ); //entire content searched

            $hilited = $FUNCS->hilite_search_terms( $keywords, $rec['content'] );
            $CTX->set( 'k_search_excerpt', $hilited ); //hilighted excerpt of searched content
        }
    }

    $FUNCS->add_event_listener( 'alter_tag_search_execute', array('SearchEx', 'tag_handler') );

/////////// end <cms:search_ex> ///////////

有了上面的代码,它直接替换了核心的 cms:search 标签,例如

这样可以支持2个字符的搜索,理论上甚至可以通过将 2 改为任意数字来控制搜索字符的长度。

最后一个问题:

目前低于指定字符的情况,我们实际搜索不出来结果,也没有任何提示,需要做一点工作。

第一种方式就是在没有结果的情况下提示“无结果”,但是这个是字符限制并没有直接搜索。

我们只能通过搜索输入阶段对搜索输入框进行限制验证提示,参考一下代码:

<cms:input type="text" name="name" label="搜索" desc='至少三个字符' maxlength="100" required='1' validator='min_len=3'
validator_msg='required = 请输入一些文字 | min_len=太短了!'
/>

当然我们可以改成2个。

------------

接下来发现中文站很多无法搜索,因为英文单词是每个单词有空格隔开的,中文却没有,这是本系统没有做中文分词搜索的问题。
一个巧妙的方式可以解决此问题:

1. 添加一个文本字段,让里面填入主要可以搜索关键词,用空格隔开。
2. 其实和第一条一样,每个克隆类型做tags管理。
3. 通常我们做全每页的TDK就行了,比如下面的代码在每页增加

<cms:editable name='page_title' label='页面SEO标题' type='text' order='210'></cms:editable>
<cms:editable name='page_desc' label='页面SEO描述' type='text' order='220'></cms:editable>
<cms:editable name='page_keywords' label='页面SEO关键词' type='text' order='230'></cms:editable>

这样所有信息就可以检索了。