发心
全文搜索系统并不是什么特别新的技术,在网络搜索引擎普及之前就已存在,搜索的概念更是一个我们时刻都会用到的技术,如最基础的数据库模糊查询 到正则匹配,这些都是我们会用于在搜索上的基本技能。然而系统的复杂性,性能的高效性,都不停地促进我们技术的进步与发展,所以才会出现很多知名的 搜索系统如ES、Lucene、Solr等。最早时候接触过Sphinx/coreseek搜索系统,它的出现就是解决简单数据模糊查询等简单搜索技术的性能劣势,以及匹配的 准确性,同时真正把搜索做到系统层面,后面很多搜索系统也都受其影响。nsearch全文搜索引擎是我新造的轮子,它是配合nmid(介绍链接)微服务系统生态的一员,作为nmid的worker端提供分布式 搜索服务。
介绍
nsearch项目地址
https://github.com/HughNian/nsearch
nsearch是由Golang开发的全文搜索引擎,可以单机本地使用也通过配合nmid微服务调度系统实现分布式架构使用。
目前有几个大模块构成
- 分词器:这里用的是npw(npartword)分词系统,通过nmid微服务调度系统进行调用,此时nsearch作为客户端使用npw服务,具体可以 在这里查看npw的介绍(介绍连接)
- 索引器:搜索引擎里面很重要的概念就是索引的概念,同样在数据库mysql这些软件里我们也用到,索引的作用就是用来快速查找内容。nsearch会把存入的内容分词后进行倒排索引,就是通过分词的关键词关联内容。这里 有用到btree数据结构,进行构建倒排索引表。
- 储存器:由于搜索引擎搜索内容可能会非常多,而这些内容最开始时都存放在btree的倒排索引表中,这样会造成内存空间使用大大增加,所以这时需要把内存中的倒排索引数据存入持久层存储中,
这样算是用时间换空间了,毕竟内存中的速度要快。持久层存储用到了golang的
boltdb
和sqlite
两种,你可以自己配置需要使用的持久存储,二选一。默认数据存放在btree的内存中,可以通过调用 刷新索引操作,将数据存入持久层。 - 排序器:搜索引擎的结果都是需要进行筛选和排序的,因为你的输入关键词决定你搜索的结果,只有与你的关键词匹配度越高,搜索的结果才能越准确,搜索引擎才能是好搜索引擎。 这里就需要对你的关键词和文本数据进行相关性算法计算了,nsearch这里用到了tf-idf,bm25算法,再通过bm25的值进行排序,同时可以进行结果分页处理。
使用
//golang调用示例
package main
import (
"github.com/vmihailenco/msgpack"
cli "github.com/HughNian/nmid/client"
"log"
"fmt"
"os"
)
const SERVERHOST = "xxx.xxx.x.xxx"
const SERVERPORT = "xxxx"
func main() {
var client *cli.Client
var err error
serverAddr := SERVERHOST + ":" + SERVERPORT
client, err = cli.NewClient("tcp", serverAddr)
if nil == client || err != nil {
log.Println(err)
return
}
defer client.Close()
client.ErrHandler= func(e error) {
if cli.RESTIMEOUT == e {
log.Println("time out here")
} else {
log.Println(e)
}
fmt.Println("client err here")
}
respHandlerIndex := func(resp *cli.Response) {
if resp.DataType == cli.PDT_S_RETURN_DATA && resp.RetLen != 0 {
if resp.RetLen == 0 {
log.Println("ret empty")
return
}
var retStruct cli.RetStruct
err := msgpack.Unmarshal(resp.Ret, &retStruct)
if nil != err {
log.Fatalln(err)
return
}
fmt.Println(retStruct.Msg)
return
}
}
respHandlerSearch := func(resp *cli.Response) {
if resp.DataType == cli.PDT_S_RETURN_DATA && resp.RetLen != 0 {
if resp.RetLen == 0 {
log.Println("ret empty")
return
}
var retStruct cli.RetStruct
err := msgpack.Unmarshal(resp.Ret, &retStruct)
if nil != err {
log.Fatalln(err)
return
}
if retStruct.Code != 0 {
fmt.Println(retStruct.Msg)
return
}
fmt.Println(string(retStruct.Data))
fmt.Print("\n\n")
}
}
//添加、更新索引
docId := "1"
docType := "1"
content := "文本"
text := []string{docId, docType, content}
params, err := msgpack.Marshal(&text)
if err != nil {
log.Fatalln("params msgpack error:", err)
os.Exit(1)
}
err = client.Do("IndexDoc", params, respHandlerIndex)
if nil != err {
fmt.Println(err)
}
//删除索引
docId := "1"
docType := "1"
content := "文本"
text := []string{docId, docType, content}
params, err := msgpack.Marshal(&text)
if err != nil {
log.Fatalln("params msgpack error:", err)
os.Exit(1)
}
err = client.Do("DelIndexDoc", params, respHandlerIndex)
if nil != err {
fmt.Println(err)
}
//刷新索引
text := []string{"1"}
params, err := msgpack.Marshal(&text)
if err != nil {
log.Fatalln("params msgpack error:", err)
os.Exit(1)
}
err = client.Do("FlushIndex", params, respHandlerIndex)
if nil != err {
fmt.Println(err)
}
//全文搜索
query := "xxx"; //关键词
mode := "1"; //搜索精准度 1-模糊,2-精准
page := "1"; //结果分页
limit := "10"; //分页展示条数
stext := []string{query, mode, page, limit}
params9, err := msgpack.Marshal(&stext)
if err != nil {
log.Fatalln("params msgpack error:", err)
os.Exit(1)
}
err = client.Do("NSearch", params9, respHandlerSearch)
if nil != err {
fmt.Println(err)
}
}
php调用示例
$host = 'xxx.xxx.x.xx';
$port = xx;
$this->client = new ClientExt($host, $port);
$this->client->connect();
$docId = 1; //自定义内容id
$docType = 1; //自定义内容类型type值
$content = "文本" //自定义内容
//添加、更新索引
$params = msgpack_pack(array("{$docId}", "{$docType}", "{$content}"));
$return = array();
$this->client->dowork("IndexDoc", $params, function($ret) use (&$return) {
if($ret[0] != 0) {
$return = "error";
} else {
$return = $ret[1];
}
});
//删除索引
$params = msgpack_pack(array("{$docId}", "{$docType}", "{$content}"));
$return = array();
$this->client->dowork("DelIndexDoc", $params, function($ret) use (&$return) {
if($ret[0] != 0) {
$return ="error";
} else {
$return = $ret[1];
}
});
//刷新索引
$params = msgpack_pack(array("1"));
$return = array();
$this->client->dowork("FlushIndex", $params, function($ret) use (&$return) {
if($ret[0] != 0) {
$return = "error";
} else {
$return = $ret[1];
}
});
//全文搜索
$query = "xxx"; //关键词
$mode = "1"; //搜索精准度 1-模糊,2-精准
$page = 1; //结果分页
$limit = 10; //分页展示条数
$params = msgpack_pack(array("{$query}", $mode, "{$page}", "{$limit}"));
$return = array();
$this->client->dowork("NSearch", $params, function($ret) use (&$return) {
if($ret[0] != 0) {
$return = "error";
} else {
$return = $ret[2];
}
});
最后
后面还会不断完善nsearch的相关文本匹配算法,以及存储数据结构的优化,增加词向量,联想功能。如果对索引技术比较感兴趣的话,可以看看黑夜路人大佬整理的有关全文 搜索技术的这篇文章戳我!