• bm25模型
  • 查询时权重提升
  • 修改查询结构
  • 修改评分
    • constant_score查询
    • 函数评分 function_score
    • 脚本评分 script_score
    • 更改相似度模型
  • 多字段排序

前面介绍了es的分词原理,分词决定了如何找到匹配的文档,而在全文搜索引擎里面,结果的展示相当重要,不仅要找到结果,还要对结果进行适当的排序.

es通过相似度对文档进行打分,默认情况下按照评分高低排序,不过如果使用term精确查询的话就不会计算相似度.

BM25算法

es在5.0之后默认采用的相似度算法是BM25,而需要注意的是官方中文文档是基于2.x版本写的,当时使用的是tf/idf算法.

bm25公式如下

bm25公式

公式的详细介绍可以参考lucene官网

https://lucene.apache.org/core/8_0_0/core/org/apache/lucene/search/similarities/BM25Similarity.html

不过这里并不打算深入分析这条公式,我们可以通过几个简单的概念学会如何利用它

f(qi,D)表示的是一个词对一个文档的相关性的权重,es使用的公式是idf.

在idf中有三个概念

  • 词频: tf(term frequency),该词在文档中出现的次数越多该值越高
  • 逆向文档频率: idf(inverse document frequency),该词在索引中所有文档出现的次数越多该值越低,可表示关键词的特异程度
  • 字段长度归一化(field-length norm): 字段长度越短,字段权重越高,表示关键词在字段中的占比

也就是tf越高,idf和字段长度归一化越低,文档的评分越高

在tf/idf中并没有对词频上限作出限制,因此当关键词在某个字段大量出现时文档的得分就会非常高,但是这些得分几乎都来自这个字段,相当于这个字段的权重被提高了,其他字段的影响被削弱了.

所以bm25引入了k1和b两个可调节参数来惩罚上述的情况. k1控制着词频结果在词频饱和度中的上升速度.默认值为1.2,值越小饱和度变化越快.

b控制着字段长度归一化的作用.0表示禁用归一化,1表示完全归一化,默认值是0.75

下面可以看到在词频上升过程中两种模型的曲线变化

bm25曲线

调整排序

下面介绍一些调整文档最终排序的方法

查询时权重提升

当进行多字段查询时,可以用boost独立控制每个字段的权重.要注意的是权重的比例不直接等于最终评分的比例.

GET /_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": {
              "query": "quick brown fox",
              "boost": 2 
            }
          }
        },
        {
          "match": { 
            "content": "quick brown fox"
          }
        }
      ]
    }
  }
}

修改查询结构

es的几种类型的查询是可以自由组合的

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "term": { "text": "quick" }},
        { "term": { "text": "brown" }},
        { "term": { "text": "red"   }},
        { "term": { "text": "fox"   }}
      ]
    }
  }
}

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "term": { "text": "quick" }},
        { "term": { "text": "fox"   }},
        {
          "bool": {
            "should": [
              { "term": { "text": "brown" }},
              { "term": { "text": "red"   }}
            ]
          }
        }
      ]
    }
  }
}

constant_score查询

有时候我们想使用分词,但又不希望进行复杂的评分,就可以通过constant_score来禁用bm25,这样每个命中字段的得分都是1

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "constant_score": {
          "query": { "match": { "description": "wifi" }}
        }},
        { "constant_score": {
          "query": { "match": { "description": "garden" }}
        }},
        { "constant_score": {
          "query": { "match": { "description": "pool" }}
        }}
      ]
    }
  }
}

上面是官网中文文档的例子,不过在6.x版本之后,constant_score里面不能使用query查询,只能使用filter,但由于filter本身恰好是不打分的,所以constant_score现在很少会使用了.

GET /_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "user.id": "kimchy" }
      },
      "boost": 1.2
    }
  }
}

function_score查询

如果想使用自定义的公式替代bm25,则可以使用function_score.

function_score允许为每个主查询的评分额外应用一个函数,可以修改或者完全替换原来的公式.

内置的函数包括

  • weight : 为每个文档应用一个简单而不被规范化的权重提升值:当 weight2 时,最终结果为 2 * _score
  • field_value_factor :使用这个值来修改 _score ,如将 popularityvotes (受欢迎或赞)作为考虑因素。最终结果为 field_value_factor * _score
  • random_score :为每个用户都使用一个不同的随机评分对结果排序,但对某一具体用户来说,看到的顺序始终是一致的。
  • script_score : 自定义脚本,最终武器,脚本里可以使用tf,idf等变量
GET /blogposts/post/_search
{
  "query": {
    "function_score": { 
      "query": { 
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": { 
        "field": "votes" 
      }
    }
  }
}

上面的语句表示查询在title和content里搜索和’popularity’相关的内容,并且votes越高评分越高,最终评分为_socore * votes.

上面的查询还可以改进下,如果点赞数太高,可能相关度低的文章也会排名靠前,可以对点赞数取log,此外votes有可能为空值,得加下默认值

GET /_search
{
  "query": {
    "function_score": {
      "field_value_factor": {
        "field": [ "title", "content" ],
        "factor": 1.2,
        "modifier": "log1p",
        "missing": 1
      }
    }
  }
}

function_score默认是与原始评分乘算,可以用boost_mode来改写成加算

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p",
        "factor":   0.1
      },
      "boost_mode": "sum" 
    }
  }
}

script_score查询

GET /_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "message": "elasticsearch" }
      },
      "script_score": {
        "script": {
          "source": "Math.log(2 + doc['my-int'].value)"
        }
      }
    }
  }
}

source里面是自定义脚本,可以用java或者groovy语句

一个简单的利用tf和idf的脚本如下

{
    "script": {
        "source": "double tf = Math.sqrt(doc.freq); double idf = 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;"
    }
}

脚本里可以利用的上下文变量可参考官网https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-similarity-context.html

更改相似度模型

可以在索引设置里自定义相似度算法,内置的算法有BM25,DFR,DFI,IB等

PUT /my_index
{
  "settings": {
    "similarity": {
      "my_bm25": { 
        "type": "BM25",
        "b":    0 
      }
    }
  },
  "mappings": {
    "doc": {
      "properties": {
        "title": {
          "type":       "string",
          "similarity": "my_bm25" 
        },
        "body": {
          "type":       "string",
          "similarity": "BM25" 
        }
      }
    }
  }
}

上面自定义了一个bm25算法,b=0表示禁用了归一化

多字段排序

es默认使用_socre排序,有时候搜索结果可能有很多文档得分相同,需要加些规则让它们在这种情况下也能排序固定

{
    "sort": [
        {
            "_score": {
                "order": "desc"
            }
        },
        {
            "time": {
                "order": "desc"
            }
        },
        {
            "_id": {
                "order": "desc"
            }
        }
    ]
}