先交代几个概念:

1. 题库

题库中保存了所有的题目数据。

2.es

es 是 elasticsearch ,es 同步了题库中的所有题目。

3. 重题

两道题目内容完全一样的题,肉眼看起来是一样的。题目里有 latex 公式,不同的 latex 公式展示后看起来可能是一样的。

4. es 打分

es 会对匹配到的结果集进行打分,分值越高的结果越排在前面。

另外,还涉及到分词、倒排索引这两个概念,这是搜索引擎的基本原理。

5. 分词

一段文本在保存到倒排索引之前需要先分词,查询的时候都是按分词来匹配的。

英文分词比较简单,基本上是以空格作为分隔符进行分词的,一个英文单词就是一个分词。

咱们系统用的分词器是 ik 分词器,ik 分词器是依靠静态的字典来分词的,算法比较简单但还很有效。ik 分词器没有自学习的功能,所以他发现不了新词,但是可以在扩展字典里添加新词。

下面是截取的 ik 词典里的部分词语:

一一列举
一一对应
一一道来
一丁
一丁不识
一丁点
一丁点儿
一七八不
一丈
一上
一上去
一上台
一上场
一上来
一下
一下下
一下儿
一下去
一下台
一下场
一下子

字典里保存的都是词语也有个别的单个字,假如现在有一句话:

我爱你中国

假如字典是:

我爱你
中国

那么分词后的结果就是:
我爱你 中国 。
假如字典是:

爱你
中国

那么分词后的结果就是:
我 爱你 中国。

如果字典里找不到的汉字,就把单个汉字作为一个分词了。只有相邻的汉字才需要去字典里查词来决定怎么分词,如果汉字之间用空格隔开,那么每个汉字就是单独的一个分词。
不管字典里的内容是如何的:

我 爱 你 中 国

用空格分隔开了所有的汉字,分词后的结果也是每个字都是单独的分词。

ik 分词器基本上就是依赖字典来分词的。

6. 倒排索引

见我的博客elasticsearch lucene 倒排索引

1. 重题搜索的业务背景:

需要人工把老师给的试卷(word、pdf、或者图片组成的 pdf) 文档录入到题库中。在录题的时候首先 ocr 会识别题目内容,然后根据识别的结果去 es 中查询,查询是否库里已经有了这道题。
如果有了的话,就可以直接用,不用再重新录入了。如果已经有了同样的题,还重新录入的话,会有几个缺点:

  1. 录题浪费时间,尤其是公式的录入浪费时间。
  2. 录题之后还有审题题目、校对题目的流程,这些流程都需要耗费时间,审核、校对都是计件收费,也会有金钱的成本。
  3. 同样内容的题在库里出现多次,但是 id 不一样,在给老师学生根据相似题推荐题目的时候,可能会推荐出看起来完全一样,仅仅是 id 不同的题。

2. 重题搜索的难点:

  1. 难点在回字有多种写法,题目索引进 es 中的时候需要规范化,同样的展示方式应该统一成一种。以数学为例,同一个公式或者符号有多种写法。但是展示出来的时候却是一样的。
    举个例子,同样的一个公式符号△就有两种不同的写法。
    在这里插入图片描述

    再举一个例子,同样的公式,在不同的上下文下展示出的效果是不一样。

       \left{ 
       \left\{  
       \{  
       \lbrace
    

    这 4 个公式的展示效果是一样的,都是 { 。不过 \left{ 、\left{ 这两个公式在有的公式编辑器里可以展示,有的却不行。
    在这里插入图片描述

    同样的 { 在这儿却又是不可见的字符了,作用仅仅是把多个数字组织在一起当做一个整体。
    同样的字符在不同的上下文的意义不一样,所以针对这些特殊的字符得在不同的上下文做不同的处理。

  2. 难点在 ocr 识别的时候会有错误。重题搜索的第一步是 ocr 识别图片中的内容,然后转化为文本。ocr 中文识别的准确率比较高,但也会把一些形近字识别错。识别错误类型可以分为 4 类:

    1. 识别多出来一些文字,比如把题干中的图片中的一些文字识别出来了。
      在这里插入图片描述
      在这里插入图片描述

    2. 形近识别错误:

      • 比如数学中的集合识别为集台。
      • 把 l 识别为 1 。
      • 把 √ 识别为 V 。
      • 把数学中的并集符号 ∪ 识别为英文字母 U 。
      • 把数学中的 λ 符号识别为中文的 入 。
    3. 识别内容丢失:例如把 {} 还有一些数字也丢掉了。
      在这里插入图片描述 在这里插入图片描述

    4. 识别的内容的顺序乱了。多行的内容识别以后就乱序了,和题目录入的顺序是不一样的。
      在这里插入图片描述
      在这里插入图片描述

  3. 难点在 es 中文索引的时候用的是 ik 分词器,ik 只保留中文、英文字母、数字,个别的特殊字符。很多数学中的公式字符都被忽略了,比如 △、+、= 等。

  4. 难点在 es 的搜索结果集中把重题排在第一位。有好多相似题,尤其是数学题,仅仅是函数的数字不一样而已,但是需要把重题排在前 3 位,不光要把重题搜索出来,难点在于怎么样能把重题排在最前面,最好是排在第一位。

3. 之前的重题搜索为什么效果不够好?

  1. 因为没有对一些常见的数学中的特殊符号做处理,例如 △、+、= 等,导致了 ik 在索引的时候把一些关键信息丢掉了。信息越多匹配出来的结果就越接近于重题,丢失掉了部分信息效果就会差些。

  2. 原来用的匹配算法是根据题目内容匹配和公式匹配两部分来决定相似度的。

      {
      "size": 15,
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "exerciseContent": {
                  "query": "中已知且则是A直角三角形B等边三角形D钝角三角形C等腰直角三",
                  "operator": "OR",
                  "prefix_length": 0,
                  "max_expansions": 50,
                  "fuzzy_transpositions": true,
                  "lenient": false,
                  "zero_terms_query": "NONE",
                  "auto_generate_synonyms_phrase_query": true,
                  "boost": 1
                }
              }
            },
            {
              "bool": {
                "should": [
                  {
                    "exists": {
                      "field": "latex",
                      "boost": 1
                    }
                  },
                  {
                    "match_phrase": {
                      "latexContent": {
                        "query": """\( ( \frac {\overrightarrow {AB}} { \overrightarrow {AB} } + \frac {\overrightarrow {AC}} { \overrightarrow {AC} } ) \overrightarrow {BC} 0 \)""",
                        "slop": 1,
                        "zero_terms_query": "NONE",
                        "boost": 2
                      }
                    }
                  },
                  {
                    "match_phrase": {
                      "latexContent": {
                        "query": """\( \frac {\overrightarrow {AB}} { \overrightarrow {AB} } \frac {\overrightarrow {BC}} {\mid \overrightarrow {BC \mid}} - \frac {\sqrt 2} 2 \)""",
                        "slop": 1,
                        "zero_terms_query": "NONE",
                        "boost": 2
                      }
                    }
                  },
                  {
                    "match_phrase": {
                      "latexContent": {
                        "query": """\( \Delta ABC \)""",
                        "slop": 1,
                        "zero_terms_query": "NONE",
                        "boost": 2
                      }
                    }
                  }
                ],
                "adjust_pure_negative": true,
                "boost": 1
              }
            }
          ],
          "adjust_pure_negative": true,
          "boost": 1
        }
      },
      "_source": false,
      "sort": [
        {
          "_score": {
            "order": "desc"
          }
        },
        {
          "exerciseId": {
            "order": "desc"
          }
        }
      ]
    }
    
    
  3. 根据题目内容匹配,ik 分词器会先对题目内容进行分词,只要匹配到任意一个分词就能到了结果集中。候选结果集中的记录在每个分词上都有相应的得分,把所有的得分求和后就是最终分。最终分越高的越排在前面。

    1. 每个分词的分值的影响因素:
      1. 这个分词在候选记录中出现的次数越多,分值就越高,在一段文本中反复出现某个词,es 就认为这段文本很可能就是用户想要查询的。这个分词在这次查询中占的分量就相对越重。
      2. 这个分词在所有的记录中出现的次数越多,几乎每句话中都出现,例如中文的"的",几乎句句话离不开的,所以出现次数越多,这个词在这次查询中占的分量反而越轻。
    2. 使用 es 的 match 匹配来查询符合我们的业务场景吗?我们的业务场景希望是完全一样的文本排在最前面,和分词在候选文档中出现的次数没有关系。和一个分词在所有文档中出现的次数也没有关系。下面是我构造的一个索引。
     id  text
     1    我 爱 你
     2    我 我 爱 你
     3    我 我 爱 爱 你
     4    我 我 爱 爱 你 你
     5    我 我 我 我 爱 爱 爱 你 你 你
    
    

    下面是查询 dsl 语句:

    {
      "query" : {
        "match": {
          "text": "我 爱 你"
        }
      }
    }
    

    下面是查询结果集:

    {
      "took": 2,
      "timed_out": false,
      "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": 5,
        "max_score": 0.36367953,
        "hits": [
          {
            "_index": "test_tmp_shfq",
            "_type": "_doc",
            "_id": "5",
            "_score": 0.36367953,
            "_source": {
              "text": "我 我 我 我 爱 爱 爱 你 你 你"
            }
          },
          {
            "_index": "test_tmp_shfq",
            "_type": "_doc",
            "_id": "4",
            "_score": 0.35185343,
            "_source": {
              "text": "我 我 爱 爱 你 你"
            }
          },
          {
            "_index": "test_tmp_shfq",
            "_type": "_doc",
            "_id": "3",
            "_score": 0.3377158,
            "_source": {
              "text": "我 我 爱 爱 你"
            }
          },
          {
            "_index": "test_tmp_shfq",
            "_type": "_doc",
            "_id": "2",
            "_score": 0.32714987,
            "_source": {
              "text": "我 我 爱 你"
            }
          },
          {
            "_index": "test_tmp_shfq",
            "_type": "_doc",
            "_id": "1",
            "_score": 0.3222385,
            "_source": {
              "text": "我 爱 你"
            }
          }
        ]
      }
    }
    

    从查询结果中可以看出,我们实际想要的是 “我 爱 你” ,但是 es 却把这条记录放到最后一个了。从理论和实际都可以看出来这种匹配算法不适合我们的业务场景。

  4. 如果传了 latex 公式,就会按 match_phrase 查询来匹配。

    1. 分析:latex 公式,实际还是先 ocr 识别,然后再转换为 latex 公式,这个过程中可能会把公式给转换错误。一旦转换错误,就可能把不是重题的题排在最前面,公式的匹配权重设置的比文本的要高一倍。一旦误匹配就容易导致排序出问题。虽然不同的题公式可能不太一样,所以公式的权重高,区分度更高,但是因为识别容易出错,一旦出错就会误匹配,把不是重题的题排在第一位了,真正的重题却排在了后面。
    2. 结论:按公式 match_phrase 来搜索也不太合适。

4. 怎样的匹配算法才是合理的?

分析:
  1. 两个题是重题,那么组成这两个题的分词集合应该是一样的,两个集合的交集越大并且差集越小就越可能是重题。理论上这两个集合的交集应该等于这两个集合本身,它们的差集应该是空集合。两个集合 A 、B,如果 A 是 B 的子集,并且 A 的元素个数和 B 的元素个数是相同的,那么这两个集合就一定是相同的集合。差集越小其实就是两个集合的元素个数相差的越小。
  2. 两个重题的字符顺序也应该是一样的。例如:我爱你 和 你爱我 组成这两句话的词语集合是一样的,但是字与字之间的顺序可不一样。
结论:
  1. 尽可能的让查询文本的分词集合和待匹配的记录的分词集合的交集大。
  2. 尽可能的让查询文本的分词集合和待匹配的记录的分词集合的元素个数相同。如果不解决这个问题,只追求两个集合的交集最大,想要查询"我爱你",可能给出的结果却是"我爱你张三"、"我很爱你"这样的记录。前者是后者的真子集。而不是相同的集合,需要把这种真子集的情况尽可能给排除掉。
  3. 尽可能的保证查询文本的顺序和待匹配的记录的文本内容的顺序也是一样的。
实现:
1. 怎样判断两个集合的交集的大小?

理想情况下给每个词赋值 1 分都可以。然后对分值求和,匹配到越多的分词,和越大,两个集合的交集也越大。

实际这样做的话则不是最优的,因为 ocr 可能会有识别错误,虽然这种概率比较小。

合理的做法应该是把那些识别率高的词赋予的分数高。汉字的识别率高,汉字的分数给的就比其他数字、字母的要高。汉字被识别错误的概率比较低,识别错误之后,相邻的汉字还能组成分词,这种概率就更小了。所以,我们就建立了一个专有名词词库,词库里的词的长度都大于 1 。比如集合被识别错为集台,而集台肯定不在关键字的字库里。如果有一个词是"集合",那么几乎可以肯定查询的原文中有集合两个字。所以由多个字组成的词语赋予的分值应该更高。

给词库中的词赋予的分值是不是越高就越好?也不是。因为赋予的分值一旦太高的话,一个错误就能大幅度的影响到结果集的排序。例如,有些题目中有图片,图片中有文字,图片中的文字就成了干扰因素,如果图片中的文字被赋予了很高的分值就可能把错误的结果给排在最前面了。

每个词的分值赋予的不能太高,也不能太低,太高、太低都会对结果集的影响太大,因为一个错误就对整体造成了很大的影响。合理的排序应该是由多个因素共同造成的,个别的错误不应该影响到最终的结果。

2. 怎样才能判断出两个集合元素个数相近?

ocr 中文识别准确,基本上也不会丢中文,公式、数字、数学符号会丢掉不少。由于 ocr 中文识别比较准确,不会丢失。所以在同步题目建立索引的时候就可以把题目中的中文个数作为题目的信息保存到索引中。

在 ocr 识别文本后搜索重题的时候再计算一下文本中的中文个数,然后满足不同的条件给出不同的分值:x 是待搜索的记录的中文个数,n 是查询文本的中文的个数。总的原则是二者的中文个数越接近,赋予该记录的分值则越高。
在这里插入图片描述

3. 怎样来保证查询的文本的顺序和记录的文本顺序是一致的?

假如查询的是:

我爱你中国

这样的文本,如果每个字是一个词,分词结果是:

我 爱 你 中 国

那么:

我爱你中国
中国我爱你

这两句话的得分是一样的,没法判断哪个更接近查询的文本。
如果整句话是一个分词:

我爱你中国

则:

中国我爱你

就匹配不到了。

从直观上可以看出,分词的词语越长,就越能保证文本的顺序是一致的。所以为了提高文本的顺序匹配性,可以在词库中加入一些常见的词汇。
例如:

充分不必要条件
必要不充分条件

构成这虽然这两个词语的汉字是一样的,但是顺序却不一样。
如果分词是:

充分 不 必要 条件

这样分的话,这两句话的得分就一样了。

这是通过往词库里加入比较长的关键字来实现查询文本的顺序尽可能的和索引记录中的文本的顺序是一致的。

还有一种办法是在 es 查询出结果之后,再根据算法对结果集进行排序。
把查询文本中的中文、数字、其他字符都提取出来,然后再把结果集中的每条记录的中文、数字、英文字符都提取出来。

然后利用相似度算法对查询文本中的中文和记录中的中文、查询文本中的数字和记录中的数字、查询文本中的英文字符和记录中的英文字符进行相似度算法比较。
由于中文识别率高,并且识别后也不容易乱序。所以对中文相似度给的权重比较高。
数字、其他字符容易丢失、错乱,所以给的权重比较低。

最终是按 sum = es 得分 + 中文相似度得分 + 数字相似度得分 + 英文相似度得分 这四个维度得分的总和来排序,得分最高的排在最前面。

5. 本次优化是怎么解决上面提到的 4 个重题搜索的难点的?

  1. 多种回字写法的解决。
    把线上环境的几百万道题都拉到线下,遍历每道题,然后提取出每道题中的公式,然后去重。这部分公式经过肉眼分析后,把同样的展示不同的写法的公式都统一映射成同样的关键字。
    例如:
   \left{ 
   \left\{  
   \{  
   \lbrace

这几种写法展示后的效果都是 { 括号,所以把这几种都统一转换为 lbrace 了。还有一种 { 括号的作用是把相邻的数字或其他字符当做一个整体来处理,比如

\sqrt { 81 }

这种 {} 号实际是没有意义的,所以进索引的时候,这种括号应该是丢弃的。所以在这里就有个难题,不能简单的做一个替换。例如有这样两个公式:

\le
\left{

前者是后者的前缀,实际上在建索引的时候需要把 \le 转换为 le ,\left{ 转换为 lbrace 。所以替换的时候就不能简单的替换了,如果先替换 \le 为 le 则可能把 \left{ 也会替换掉。

如果想要简单的替换还必须先替换 \left{ 再替换 \le ,事实上也没有采用这种简单的替换,这种替换效率差,需要遍历所有的可能情况,而且还可能替换出错。在调研 es 了解 es 分词原理的时候读了下 ik 分词器的源码。源码中用了前缀树,前缀树正好可以解决这个问题。前缀树在匹配的时候越长的越优先,我们替换的时候也是优先匹配最长的词,匹配到最长的词后就替换。前缀树基本上需要扫描一次就够了,和可能需要替换的公式的个数没有关系。

下面是建立的一个映射表的部分,-》 左边的词是源词,右边的是目标词。对应同样的一个目标词的多个源词用空格隔开。例如公式中出现了 ∈ 或者 \in 最终都转换为 in 了。这样的话不管录题的时候是录入 ∈ 还是 \in 在搜索的时候就都能搜到了。

∈ \in -》 in
△ \triangle \vartriangle -》triangle
+ \plus -》plus
- \minus -》minus
∞ \infty -》infty
< \lt -》lt
<= ≤ \le \leq  \leqslant -》le
= \equals -》equals
> \gt -》gt
>= ≥  \ge \geq \geqslant -》ge
  1. ocr 识别错误的解决。
    这种错误没办法直接去纠正,但是以用适当的策略来降低 ocr 识别错误对整体造成的影响。

例如中文、英文、数字的识别正确率不一样。中文的识别率最高,并且中文识别后字符的顺序也不会发生错乱。基于这一点,就可以给中文赋予更高的权重,如果英文、数字识别错误或者错乱也不至于对整体结果产生大的影响。局部的错误不会干扰到最终的结果。

因为识别错误是小部分的,只要不要让这小部分的错误起到了决定性的作用的话,也就不会干扰到最终的结果了。基于这一点,在给分词赋予分值的时候,不同的分词赋予的分值相差就不能太大。最终的结果是由多个分词、多个策略共同来决定的。这样的话,个别的错误就不至于影响到了最后的结果。

  1. 解决 ik 分词器丢掉特殊符号的问题。

之前看过 ik 分词器的源码,了解到 ik 分词基本上只会保留中文、英文、数字,再有可能就是小数点等极少数的符号。而实际上数学有大量的符号,这些符号的区分度很高,把这些区分度很高的关键字丢失了,肯定会降低重题搜索的质量。

这是部分数学符号:

∈
△
+
-
∞
<
<=
≤
=
>
>=
π
{
∵
∴
≠ 
β
∋
GET _analyze
{
  "analyzer": "ik_smart",
  "text": ["∈ △ + - ∞ < <= ≤ = > >= π { ∵ ∴ ≠  β ∋"]
}

这是 ik 分词器对这些符号的分词结果:

{
  "tokens" : [ ]
}

从结果上看,ik 也是确实把这些特殊的符号都丢掉了。

现在我们需要把这部分特殊的符号映射成英文,这样 ik 在分词的时候就丢不掉了。

下面是一份映射表的部分:

∈ -》 in
△  -》triangle
+ -》plus
- - -》minus
∞ -》infty
<  -》lt
<= ≤ -》le
= -》equals
> -》gt
>= ≥   -》ge
⊥ -》bot

π -》pi
→ -》rightarrow
{ -》lbrace
} -》 rbrace

和前面的映射表的格式是一样的,把特殊字符映射成了英文单词。这样映射后,特殊符号和 latex 公式就可以查询到了。在录入 ∈ 符号的时候可以用 latex 公式 \in 来录入。在对题目内容进行索引的时候保存的是 in 。在 ocr 识别 ∈ 后最终交给 ik 分词器分词前就已经转换为了 in ,这样一来就能在特殊符号和公式符号之间查询了。

实际上有这么两张映射表,一张映射表是 ocr 识别后去查询的映射表,这张映射表中的所有字符都肯定是可见的。而在同步题目数据建索引的时候用到的映射表,有些公式或者符号是不可见的,所以需要把这部分内容替换为空。

  1. 解决排序的问题

排序已经到了最后一步了,前面做的所有工作都对排序的准确性做了贡献。

在 es 查询出结果之后,会根据算法对结果集进行排序。
把查询文本中的中文、数字、其他字符都提取出来,然后再把结果集中的每条记录的中文、数字、英文字符都提取出来。

然后利用相似度算法对查询文本中的中文和记录中的中文、查询文本中的数字和记录中的数字、查询文本中的英文字符和记录中的英文字符进行相似度算法比较。
由于中文识别率高,并且识别后也不容易乱序。所以对中文相似度给的权重比较高。
数字、其他字符容易丢失、错乱,所以给的权重比较低。

最终是按 sum = es 得分 + 中文相似度得分 + 数字相似度得分 + 英文相似度得分 这四个维度得分的总和来排序,得分最高的排在最前面。

6. 最后再总结一下优化的整个流程。

整个流程实际上分为两个阶段:

1. 建立索引的过程。
  1. 在建立索引前对常用的数学专业词汇整理,形成扩展字典。
  2. 对文本进行清洗,例如去掉多余的空格,例如
    \left{
    \left {
    \left  {
    
    有些公式内部之间可以没有空格或者多个空格,最终展示的效果都是一样的。但是在替换的时候,如果不对这些多余的空格处理的话,就没法替换了,或者得增加各种 case 。
  3. 对题目内容中的公式进行标准化转换、忽略掉无效的公式字符等。
  4. 在建索引的时候把题目内容、公式内容放到一起进行处理的,没有像之前一样把文本内容和公式内容进行单独索引。
  5. 计算题目内容中的中文个数。
  6. 建立索引。
2. 查询的过程。
  1. 清理查询文本数据,去掉干扰项。
    1. 多个空格只保留一个,去掉多余的空格。
    2. 去掉汉字之间的空格。多行文本,ocr 识别后在换行的时候会加上一个空格。空格可能恰好把一个词截断成两半了。在一定程度上影响到了分词。
    3. 去掉题号。因为索引中的题目是没有题号,所以就需要把试卷中 ocr 识别后的题号去掉。
    4. 去掉分数。同理。
    5. 去掉试卷出处。同理。
    6. 去掉 A、B、C、D、 选项。由于 A B C D 还可能是集合符号等,所以还不能简单粗暴的就给去掉,根据题型、选项个数把明显是选项的 A、B、C、D、去掉了。ocr 在识别的时候经常把 A、识别成 A 把、丢掉了。
      这个时候就不好确认,这个 A 到底是集合中的 A 还是选项中的 A ,就需要结合题型、选项个数、上下文来确认了。实际上 A 由于是英文的最常用的不定冠词,A 是停用词,ik 在分词的时候把 A 给忽略了,但 B 、C、D 不是停用词,实际上最终的查询效果还是挺好的。个别的错误或者疏漏基本不会影响到最终的结果。
    7. 非关键词中的英文、数字拆成单个字符。
  2. 标准化,比如把 △ 、π 这些 ik 忽略掉的符号再转换为 triangle、pi 。
  3. 计算中文个数。
  4. 调用一遍 ik 的分词接口。
  5. 根据 ik 的分词结果,数字和英文可能会进行拆分,非关键词中的英文、数字拆成单个字符。优点是如果个别字符识别错误后不影响其他字符识别。缺点就是拆分之后粒度越细,匹配出来的结果集就越大。
    1. 中文:如果是关键字表里的则赋予比较高的分值,不是的话则较低一些。实际上还可以继续优化,根据分词的长度,越长的赋予的分值越高。
    2. 数字:如果是 > 10 的数字,则拆分成多个个位数字。如果本来就是个位数字赋予的分值要比拆分开来的单个数字的分值高。
    3. 英文:如果不是关键字里的单词则需要拆分,是的话,就不需要拆分。关键字单词赋予的分值要高一些。
  6. 对分词后的结果进行过滤,把拆分开的数字、英文字符过滤掉。在搜索的时候待匹配的记录中的分词必须要和过滤后的分词中的 70% 的内容匹配才可以进入到候选结果集中。这样的话,可以过滤掉很多相关性不大的记录,加快搜索的速度。
  7. 对中文个数匹配的赋值。搜索文本中的中文个数和待匹配的记录中的中文个数越接近则赋予的分值越高。
  8. 遍历 5 中构造的分词集合,然后构造 term 查询:匹配到这个分词,应该赋值多少分。
  9. 查询。
  10. 为了排除掉真子集的情况,例如想要查询 我爱你 结果集中却是 我爱你中国 我很爱你 排在 我爱你 的前面。然后再对中文、数字、英文分分别提取,利用相似度算法对查询文本中的中文和记录中的中文、查询文本中的数字和记录中的数字、查询文本中的英文字符和记录中的英文字符进行相似度算法比较。由于中文识别率高,并且识别后也不容易乱序。所以对中文相似度给的权重比较高。
    数字、其他字符容易丢失、错乱,所以给的权重比较低。相似度越高的得分越高。
  11. 对 es 打分、中文相似度打分、数字相似度打分、英文相似度打分这四种打分求和,这个和就是最终的打分,然后按这个最终的打分进行重新排序,分值越大的越靠前。
    查询文本:
已知集合A={0,2,4},B={2,4,6},则A∩B=B.0,6 C  D.{0,2,4,6}

下面是 dsl 查询语句:

{
  "size": 30,
  "query": {
    "bool": {
      "filter": [
        {
          "match": {
            "exerciseContent": {
              "query": "equals intersection 已知集合 rbrace 2 则 lbrace 6 0 4 b ",
              "operator": "OR",
              "prefix_length": 0,
              "max_expansions": 50,
              "minimum_should_match": "70%",
              "fuzzy_transpositions": true,
              "lenient": false,
              "zero_terms_query": "NONE",
              "auto_generate_synonyms_phrase_query": true,
              "boost": 0.0
            }
          }
        },
        {
          "range": {
            "chineseCharCount": {
              "from": 4,
              "to": 6,
              "include_lower": true,
              "include_upper": true,
              "boost": 1.0
            }
          }
        }
      ],
      "should": [
        {
          "term": {
            "chineseCharCount": {
              "value": 5,
              "boost": 5.0
            }
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "equals",
                  "boost": 1.0
                }
              }
            },
            "boost": 5.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "intersection",
                  "boost": 1.0
                }
              }
            },
            "boost": 5.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "已知集合",
                  "boost": 1.0
                }
              }
            },
            "boost": 12.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "rbrace",
                  "boost": 1.0
                }
              }
            },
            "boost": 5.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "2",
                  "boost": 1.0
                }
              }
            },
            "boost": 2.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "则",
                  "boost": 1.0
                }
              }
            },
            "boost": 3.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "lbrace",
                  "boost": 1.0
                }
              }
            },
            "boost": 5.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "6",
                  "boost": 1.0
                }
              }
            },
            "boost": 2.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "0",
                  "boost": 1.0
                }
              }
            },
            "boost": 2.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "4",
                  "boost": 1.0
                }
              }
            },
            "boost": 2.0
          }
        },
        {
          "constant_score": {
            "filter": {
              "term": {
                "exerciseContent": {
                  "value": "b",
                  "boost": 1.0
                }
              }
            },
            "boost": 2.0
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1.0
    }
  },
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    },
    {
      "exerciseId": {
        "order": "desc"
      }
    }
  ]
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐