Elasticsearch——倒排索引与分词

2020-11-09  本文已影响0人  小波同学

正排索引

文档ID到文档内容、单词的关联关系。比如书的目录页对应正排索引(指明章节名称,指明页数)用于查看章节。

倒排索引

单词到文档ID的关联关系。比如索引页对应倒排索引(指明关键词、指明页数)用于关键词查找
倒排索引是搜索引擎的核心,主要包含两个部分:
单词词典(Term Dictionary)

倒排列表(Posting List)
记录了单词对应的文档集合,由倒排索引项组成。倒排索引项包含如下信息:

分词

分词是指将文本转换成一系列单词的过程,也可以叫做文本分析,在es里面成为Analysis

分词器是Elasticsearch中专门处理分词的组件,英文为Analyzer,其组成如下:
Character Filters
针对原始文本进行处理,比如去除html特殊标记符。

Tokenizer
将原始文本按照一定规则切分为单词。

Token Filters
针对Tokenizer处理的单词进行在加工,比如转小写,删除或新增等处理。

分词器——调用顺序

Analyze_api

Elasticsearch提供了一个测试分词的api接口,方便验证分词效果,endpoint是_analyze

Elasticsearch自带分词器

中文分词

难点:

乒乓球拍/卖/完了
乒乓球/拍/买完了

常用分词系统

IK

ieba

基于自然语言处理的分词系统

HanLp

thulac

自定义分词

当自带的分词无法满足需求时,可自定义分词
通过自定义Character Filters、Tokenizer、Token Filters实现

Character Filters

Tokenizer

Token Filters

自定义分词的api

自定义分词需要在索引的配置中设定,如下所示:

分词会在如下两个时机使用:

索引时分词是通过配置Index Mapping中每个字段的analyzer属性实现的,如下:

查询时分词的指定方式有如下几种:

ik分词器安装与使用

ik分词器下载与安装

*2、解压,将文件复制到es安装目录/plugins/ik目录下即可


ik分词器基础知识

ik_max_word:会将文本做最细粒度的拆分,比如会将"中华人民共和国人民大会堂"拆分为"中华人民共和国、中华人民、中华、华人、人民、人民共和国、人民大会堂、人民大会、大会堂",会穷尽各种可能的组合。

ik_smart:会做最粗粒度的拆分,比如会将"中华人民共和国人民大会堂"拆分为"中华人民共和国、人民大会堂"。

ik分词器的使用

存储时使用ik_max_word,搜索时使用ik_smart

因为后续的keyword和text设计分词问题,这里给出分词最佳实践。即存储时时使用ik_max_word,搜索时分词器用ik_smart,这样索引时最大化的将内容分词,搜索时更精确的搜索到想要的结果。

PUT /index

{
    "mappings": {
        "peoperties":{
            "text":{
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_smart"
            }
        }
    }
}
ik分词器配置文件

ik原生最重要的两个配置文件:

一般向停用词会在分词的时候,直接被干掉,不会建立在倒排索引中。

自定义词库
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">mydict.dic</entry>
     <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords">mystopwords.dic</entry>
    <!--用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">words_location</entry> -->
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">mydict.dic</entry>
     <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords">mystopwords.dic</entry>
    <!--用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">words_location</entry> -->
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

使用mysql热更新词库

热更新

每次都是在es的扩展词典中,手动添加新词,很坑。

es不停机,我们直接在外部某个地方添加新的词语,es中立即热加载到这些新词语。

热更新方案

用第二种方案,第一种方案ik官方和社区都不建议采用,觉得不太稳定。

1、基于ik分词器原生支持的热更新方案
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">location</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<entry key="remote_ext_stopwords">location</entry>

其中 location 是指一个 url,比如 http://yoursite.com/getCustomDict,该请求只需满足以下两点即可完成分词热更新。

满足上面两点要求就可以实现热更新分词了,不需要重启 ES 实例。

可以将需自动更新的热词放在一个 UTF-8 编码的xxx.txt文件里,放在 nginx 或其他简易 http server下,当xxx.txt文件修改时,http server 会在客户端请求该文件时自动返回相应的 Last-Modified 和 ETag。可以另外做一个工具来从业务系统提取相关词汇,并更新这个xxx.txt文件。

个人体会:nginx方式比较简单容易实现,建议使用;

@RestController
@RequestMapping("/keyWord")
@Slf4j
public class KeyWordDict {

    private String lastModified = new Date().toString();
    private String etag = String.valueOf(System.currentTimeMillis());

    @RequestMapping(value = "/hot", method = {RequestMethod.GET,RequestMethod.HEAD}, produces="text/html;charset=UTF-8")
    public String getHotWordByOracle(HttpServletResponse response,Integer type){
        response.setHeader("Last-Modified",lastModified);
        response.setHeader("ETag",etag);

        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        String sql = "";
        final ArrayList<String> list = new ArrayList<String>();
        StringBuilder words = new StringBuilder();
        try {
            Class.forName("oracle.jdbc.driver.OracleDriver");
            conn = DriverManager.getConnection(
                    "jdbc:oracle:thin:@192.168.114.13:1521:xe",
                    "test",
                    "test"
            );
            if(ObjectUtils.isEmpty(type)){
                type = 99;
            }
            switch (type){
                case 0:
                    sql = "select word from IK_HOT_WORD where type=0 and status=0";
                    break;
                case 1:
                    sql = "select word from IK_HOT_WORD where type=1 and status=0";
                    break;
                default:
                    sql = "select word from IK_HOT_WORD where type=99";
                    break;
            }
            stmt = conn.createStatement();
            rs = stmt.executeQuery(sql);

            while(rs.next()) {
                String theWord = rs.getString("word");
                System.out.println("hot word from mysql: " + theWord);
                words.append(theWord);
                words.append("\n");
            }
            return words.toString();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    log.error("资源关闭异常:",e);
                }
            }
            if(stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                    log.error("资源关闭异常:",e);
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    log.error("资源关闭异常:",e);
                }
            }
        }
        return null;
    }

    @RequestMapping(value = "/update", method = RequestMethod.GET)
    public void updateModified(){
        lastModified = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
        etag = String.valueOf(System.currentTimeMillis());
    }

}

注:
updateModified方法为单独更新lastModified与etag,用于判断ik是否需要重新加载远程词库,具体关联数据库操作代码时自行扩展

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典 -->
        <entry key="ext_dict"></entry>
         <!--用户可以在这里配置自己的扩展停止词字典-->
        <entry key="ext_stopwords"></entry>
        <!--用户可以在这里配置远程扩展字典 -->
        <entry key="remote_ext_dict">http://192.168.xx.xx:8080/keyWord/hot?type=0</entry>
        <!--用户可以在这里配置远程扩展停止词字典-->
        <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
重写ik源码连接mysql/oracle更新词库
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/es?serverTimezone=UTC
jdbc.user=root
jdbc.password=yibo
jdbc.reload.sql=select word from hot_words
jdbc.reload.stopword.sql=select stopword as word from hot_stopwords
jdbc.reload.interval=5000
/**
 * @Description: 加载字典线程
 */
public class HotDictReloadThread implements Runnable {

    private static final Logger log = ESPluginLoggerFactory.getLogger(HotDictReloadThread.class.getName());

    @Override
    public void run() {
        log.info("[--------]reload hot dict from mysql");
        Dictionary.getSingleton().reLoadMainDict();
    }
}
public static synchronized void initial(Configuration cfg) {
    if (singleton == null) {
        synchronized (Dictionary.class) {
            if (singleton == null) {

                singleton = new Dictionary(cfg);
                singleton.loadMainDict();
                singleton.loadSurnameDict();
                singleton.loadQuantifierDict();
                singleton.loadSuffixDict();
                singleton.loadPrepDict();
                singleton.loadStopWordDict();

                //!!!!!!!!mysql监控线程  新增代码
                new Thread(new HotDictReloadThread()).start();

                if(cfg.isEnableRemoteDict()){
                    // 建立监控线程
                    for (String location : singleton.getRemoteExtDictionarys()) {
                        // 10 秒是初始延迟可以修改的 60是间隔时间 单位秒
                        pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);
                    }
                    for (String location : singleton.getRemoteExtStopWordDictionarys()) {
                        pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);
                    }
                }

            }
        }
    }
}
/**
 * 加载主词典及扩展词典
 */
private void loadMainDict() {
    // 建立一个主词典实例
    _MainDict = new DictSegment((char) 0);

    // 读取主词典文件
    Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_MAIN);
    loadDictFile(_MainDict, file, false, "Main Dict");
    // 加载扩展词典
    this.loadExtDict();
    // 加载远程自定义词库
    this.loadRemoteExtDict();
    //从mysql中加载热更新词典 新增代码
    this.loadMySQLExtDict();
}
/**
 * 加载用户扩展的停止词词典
 */
private void loadStopWordDict() {
    // 建立主词典实例
    _StopWords = new DictSegment((char) 0);

    // 读取主词典文件
    Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_STOP);
    loadDictFile(_StopWords, file, false, "Main Stopwords");

    // 加载扩展停止词典
    List<String> extStopWordDictFiles = getExtStopWordDictionarys();
    if (extStopWordDictFiles != null) {
        for (String extStopWordDictName : extStopWordDictFiles) {
            logger.info("[Dict Loading] " + extStopWordDictName);

            // 读取扩展词典文件
            file = PathUtils.get(extStopWordDictName);
            loadDictFile(_StopWords, file, false, "Extra Stopwords");
        }
    }

    // 加载远程停用词典
    List<String> remoteExtStopWordDictFiles = getRemoteExtStopWordDictionarys();
    for (String location : remoteExtStopWordDictFiles) {
        logger.info("[Dict Loading] " + location);
        List<String> lists = getRemoteWords(location);
        // 如果找不到扩展的字典,则忽略
        if (lists == null) {
            logger.error("[Dict Loading] " + location + "加载失败");
            continue;
        }
        for (String theWord : lists) {
            if (theWord != null && !"".equals(theWord.trim())) {
                // 加载远程词典数据到主内存中
                logger.info(theWord);
                _StopWords.fillSegment(theWord.trim().toLowerCase().toCharArray());
            }
        }
    }

    //!!!!!!!!从mysql中加载停用词 新增代码
    this.loadMySQLStopWordDict();
}
private static Properties prop = new Properties();

static {
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
    } catch (ClassNotFoundException e) {
        logger.error("error",e);
    }
}
/**
 * 从mysql中加载热更新词典
 */
private void loadMySQLExtDict(){
    Connection conn = null;
    Statement state = null;
    ResultSet rs = null;
    try{
        Path file = PathUtils.get(getDictRoot(),"jdbc-reload.properties");
        prop.load(new FileInputStream(file.toFile()));
        for (Object key : prop.keySet()) {
            logger.info("[--------]" + key +"=" + prop.getProperty(String.valueOf(key)));
        }
        logger.info("[--------]query hot dict from mysql," + prop.getProperty("jdbc.reload.sql") + "......");

        // 创建数据连接
        conn = DriverManager.getConnection(
                prop.getProperty("jdbc.url"),
                prop.getProperty("jdbc.user"),
                prop.getProperty("jdbc.password")
        );

        state = conn.createStatement();
        rs = state.executeQuery(prop.getProperty("jdbc.reload.sql"));

        while(rs.next()){
            String theWord = rs.getString("word");
            logger.info("[--------]hot word from mysql: " + theWord);
            _MainDict.fillSegment(theWord.trim().toCharArray());
        }

        Thread.sleep(Integer.valueOf(String.valueOf(prop.get("jdbc.reload.interval"))));
    }catch (Exception e){
        logger.error("error",e);
    }finally {
        if(rs != null){
            try {
                rs.close();
            } catch (SQLException e) {
                logger.error("error",e);
            }
        }
        if(state != null){
            try {
                state.close();
            } catch (SQLException e) {
                logger.error("error",e);
            }
        }
        if(conn != null){
            try {
                conn.close();
            } catch (SQLException e) {
                logger.error("error",e);
            }
        }
    }
}
/**
 * 从mysql中加载停用词
 */
private void loadMySQLStopWordDict(){
    Connection conn = null;
    Statement state = null;
    ResultSet rs = null;
    try{
        Path file = PathUtils.get(getDictRoot(),"jdbc-reload.properties");
        prop.load(new FileInputStream(file.toFile()));
        for (Object key : prop.keySet()) {
            logger.info("[--------]" + key +"=" + prop.getProperty(String.valueOf(key)));
        }
        logger.info("[--------]query hot stopword from mysql," + prop.getProperty("jdbc.reload.stopword.sql") + "......");

        // 创建数据连接
        conn = DriverManager.getConnection(
                prop.getProperty("jdbc.url"),
                prop.getProperty("jdbc.user"),
                prop.getProperty("jdbc.password")
        );

        state = conn.createStatement();
        rs = state.executeQuery(prop.getProperty("jdbc.reload.stopword.sql"));

        while(rs.next()){
            String theWord = rs.getString("word");
            logger.info("[--------]hot stopword from mysql: " + theWord);
            _MainDict.fillSegment(theWord.trim().toCharArray());
        }

        Thread.sleep(Integer.valueOf(String.valueOf(prop.get("jdbc.reload.interval"))));
    }catch (Exception e){
        logger.error("error",e);
    }finally {
        if(rs != null){
            try {
                rs.close();
            } catch (SQLException e) {
                logger.error("error",e);
            }
        }
        if(state != null){
            try {
                state.close();
            } catch (SQLException e) {
                logger.error("error",e);
            }
        }
        if(conn != null){
            try {
                conn.close();
            } catch (SQLException e) {
                logger.error("error",e);
            }
        }
    }
}

总结:

参考:
https://github.com/medcl/elasticsearch-analysis-ik

https://blog.csdn.net/qq_40592041/article/details/107856588

上一篇下一篇

猜你喜欢

热点阅读