nsearch全文搜索引擎

可以快速搭建你的全文搜索系统

Posted by hughnian on January 19, 2021

发心

全文搜索系统并不是什么特别新的技术,在网络搜索引擎普及之前就已存在,搜索的概念更是一个我们时刻都会用到的技术,如最基础的数据库模糊查询 到正则匹配,这些都是我们会用于在搜索上的基本技能。然而系统的复杂性,性能的高效性,都不停地促进我们技术的进步与发展,所以才会出现很多知名的 搜索系统如ES、Lucene、Solr等。最早时候接触过Sphinx/coreseek搜索系统,它的出现就是解决简单数据模糊查询等简单搜索技术的性能劣势,以及匹配的 准确性,同时真正把搜索做到系统层面,后面很多搜索系统也都受其影响。nsearch全文搜索引擎是我新造的轮子,它是配合nmid(介绍链接)微服务系统生态的一员,作为nmid的worker端提供分布式 搜索服务。

介绍

nsearch logo

nsearch项目地址

https://github.com/HughNian/nsearch

nsearch是由Golang开发的全文搜索引擎,可以单机本地使用也通过配合nmid微服务调度系统实现分布式架构使用。
目前有几个大模块构成

  • 分词器:这里用的是npw(npartword)分词系统,通过nmid微服务调度系统进行调用,此时nsearch作为客户端使用npw服务,具体可以 在这里查看npw的介绍(介绍连接)
  • 索引器:搜索引擎里面很重要的概念就是索引的概念,同样在数据库mysql这些软件里我们也用到,索引的作用就是用来快速查找内容。nsearch会把存入的内容分词后进行倒排索引,就是通过分词的关键词关联内容。这里 有用到btree数据结构,进行构建倒排索引表。
  • 储存器:由于搜索引擎搜索内容可能会非常多,而这些内容最开始时都存放在btree的倒排索引表中,这样会造成内存空间使用大大增加,所以这时需要把内存中的倒排索引数据存入持久层存储中, 这样算是用时间换空间了,毕竟内存中的速度要快。持久层存储用到了golang的boltdbsqlite两种,你可以自己配置需要使用的持久存储,二选一。默认数据存放在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的相关文本匹配算法,以及存储数据结构的优化,增加词向量,联想功能。如果对索引技术比较感兴趣的话,可以看看黑夜路人大佬整理的有关全文 搜索技术的这篇文章戳我!