用Sphinx 建立搜索引擎
1. 介绍
实际上 sphinx的网站上的title 说的很清楚,这个是一个 “免费开源的SQL 全文索引搜索引擎”。当然,它不是一个完整的搜索引擎,只提供索引 和 查询接口。所以,学习sphinx 主要是要学习:如何建立索引,如何调用查询接口。
他的作者只有一个人,但是,功能的确非常强大。目前,支持下面的特性:
n 高速索引(10M/s, 主流cpu配置)
n 高速查询(2-4G 文本,大概只要0.1s)
n 排序采用的BM25 和 短语相似度 相结合的排序方法,而非向量空间模型。
n 提供分布式搜索功能
n 灵活的查询接口,支持布尔、短语、词语相似度等多种检索模式;
n 可以支持单字节(GBK)和 utf8 编码的数据源
当然,sphinx 也有很多的缺点:
1. 分词比较难搞定,sphinx 是 C 写的,要嵌入分词算法,还是要做很多的工作。
一般现在用分词不是很准确的 LibMMSeg 进行切分。当然,分词不准确一般不会非常的影响搜索的体验。所以没有必要过于强调这一点。如果你要做的不是一个非常精致的搜索,sphinx 肯定是够用了。
2. 大多数开源搜索都基本上基于纯文本搜索的理论。并没有对文本的结构进行深入的挖掘。比如pagerank,html 标签标定的关键字,锚文字 等。如果,你要做一个比较好的垂直搜索引擎,可以考虑引入pagerank的机制,当然,存C 的 sphinx 肯定会降低你的开发效率。
说了这样多,只是想说和lucunece 相比,可扩展性差点。但是效率非常高。
2. 安装
http://faq.phpwind.net/answer-567
上面这个链接中有一个很好的教程,一步一步的做下去就能安装成功了。下面补充一点东西:
安装searchd 服务要用下面的命令:
Searchd -–install –c /path/to/config
注意 install 前面是两个 “-”,c前面是一个 “-”
还要一点要注意配置一个 stopwords 。方法是,建立一个stopwords 的列表,每个字用空格分开来,然后在每个index 的选项里面加一个 stopwords = /path/to/stopwords.txt
3. 索引
索引的技巧也在 http://faq.phpwind.net/answer-567 有一个例子,这个是一个典型的增量索引的例子。当然,增量索引的配置方法不只是一种。下面谈谈怎么更新增量索引。
1. 每10分钟,建立一次增量索引。千万不要每10分钟就合并一次索引,实际上,合并索引也非常的消耗时间。
2. 每天的晚上可以更新全部的索引。
增量索引不能反映删除 和 修改的内容,所以,建议主索引要经常的更新。一般来说,对于100万数据左右的情况,这样的方法都是可以的。
但是,如果数据量还要大的话,就采用分段索引。一个索引 是基本上不会改动的索引,比如,一年前的帖子了。一个索引是最近一年的帖子,最后一个索引是增量索引。虽然这样,会同时 要从三个索引里面搜索,影响了性能,但是,可以保证近期的比如 10W数据 很容易的被更新。而一年前的数据可以考虑一个月索引更新一次。对三个索引,还可以设置权重,增量索引设置最大,因为是最新的内容,往往是热点,大家比较关注的,可以靠前点。近一年的数据,排名第二,最后一年以外的数据,基本过时,可以排后面一些。
4. 搜索
搜索其实从手册中已经很明白的说明了。
下面要谈一下可能会出现问题的地方:
1.搜索的模式:
一般来说,比较常用的两种模式是:SPH_MATCH_ALL SPH_MATCH_ANY。 但是 coreseek 分装的中文搜索有比较严重的问题,特别是 SPH_MATCH_ANY 是有bug的(我在GBK模式下发现是有bug的,这里有讨论:http://www.coreseek.cn/forum/2_171_0.html ),所以我选择用:SPH_MATCH_EXTENDED 模式来代替。如果是或者的关系,可以这样做:
buildKeywords 先分词,然后再组合成一个查询表达式,进行查询。详细的看后面代码中的例子。
2.搜索索引的参数:
query 函数允许传递多个索引,不同的索引用 “|” 分开,但是,加亮函数只要传递主索引就好了,千万不要传多个。否则会返回false 。加亮函数中,还会传递一个关键字,如果是多个,就用 | 隔开。
下面是我为verycms 写的一个简单的 搜索调用接口,提供大家参考:
class Cms_Search_API
{
private $host;
private $port;
private $db;
private $indexs;
private $searchd;
private $keywords;
private $total;
function __construct($db = null, $host = 'localhost', $port = 3312, $indexs = "cmsindex|addcmsindex")
{
if ($db === null) {
global $db;
}
$this->host = $host;
$this->port = $port;
$this->db = $db;
$this->indexs = $indexs;
$index = explode("|", $this->indexs);
$this->mainIndex = $index[0];
$this->searchd = new SphinxClient();
$this->searchd->setServer($this->host, $this->port);
$this->searchd->setMatchMode(SPH_MATCH_EXTENDED);
}
function setLimits($page = 1, $limit = 10)
{
$offset = ($page - 1) * $limit;
$this->searchd->setLimits($offset, $limit, 1000);
return $this;
}
function setWhere($field, $value)
{
if (!is_array($value)) {
$value = array($value);
}
$this->searchd->setFilter($field, $value);
return $this;
}
/**
* set how to sort the search result.
*
* @param int $mode SPH_SORT_ATTR_DESC or SPH_SORT_ATTR_ASC
* @param string $sortby sort field.
*/
function setSortMode($sortby = 'postdate', $mode = SPH_SORT_ATTR_DESC)
{
$this->searchd->setSortMode(SPH_SORT_ATTR_DESC, $sortby);
return $this;
}
function find($query)
{
$result = $this->searchd->query($this->prepare($query), $this->indexs);
$ids = array_keys($result['matches']);
unset($result['matches']);
$this->total = $result['total'];
if (empty($ids)) {
return false;
}
$data = $this->fetchContents($ids);
return $data;
}
function getTotal()
{
return $this->total;
}
function getList($query, $page = 1, $sortby = null, $limit = 10, $sort_mode = SPH_SORT_ATTR_DESC)
{
$this->setLimits($page);
if ($sortby) {
$sort_mode = empty($sort_mode) ? SPH_SORT_ATTR_DESC : $sort_mode;
$this->setSortMode($sortby, $sort_mode);
}
if ($result = $this->find($query)) {
$this->buildExcerpts($result, "content");
$this->buildExcerpts($result, "title");
}
return $result;
}
function buildExcerpts(&$data, $field = "content")
{
$contents = array();
foreach ($data as $item)
{
$item = trim(strip_tags($item[$field]));
$contents[] = preg_replace("/\s+/", "", $item);
}
$contents = $this->searchd->buildExcerpts($contents, $this->mainIndex, $this->keywords);
foreach ($data as $k => &$v) {
$v[$field] = $contents[$k];
}
return $data;
}
/**
* fetch content from database;
*
* @param array $ids tids
* @return array search result
*/
private function fetchContents($ids)
{
$q = "SELECT th.tid,th.cid, th.title, th.postdate, th.hits, t.content, t.author, th.linkurl , th.url
FROM cms_contentindex th
LEFT JOIN cms_content1 t
USING (tid)
WHERE th.tid in (" . implode(",", $ids) . ")";
$result = $this->db->query($q);
$data = array();
$score = array_flip($ids);
$score_sort = array();
while ($line = $this->db->fetch_array($result))
{
$line['score'] = $score[$line['tid']];
$data[] = $line;
$score_sort[] = $line['score'];
}
array_multisort($score_sort, SORT_ASC, SORT_NUMERIC, $data);
return $data;
}
/**
* prepare the search query, and then use SPH_MATCH_EXTENDED mode to query.
*
* @param string $query string to search
* @return string of extend style.
*/
private function prepare($query)
{
$keywords = $this->searchd->buildKeywords($query, $this->mainIndex, false);
$query = array();
foreach ($keywords as $key) {
$query[] = $key["tokenized"];
}
$query = implode("|", $query);
$query = iconv("utf-8", "gbk", $query);
$this->keywords = $query;
return $query;
}
}
?>
下面是调用的例子:
require_once "global.php";
require_once "require/cms_search_api.php";
$search = new Cms_Search_API($db);
$page = 1;
$sortby = 'postdate'; //hits or null
$search->setWhere("cid", 5);
$result = $search->getList("提升6-12个月宝宝记忆力的游戏", $page, $sortby);
print_r($result);
?>