vlambda博客
学习文章列表

读书笔记《elasticsearch-blueprints》改善搜索体验

Chapter 6. Improving the Search Experience

完成搜索后,我们需要改进整个搜索体验。改善搜索体验意味着提供无忧无虑的无缝搜索,并能够在第一次尝试时为用户获得结果。在不同的情况下,这可能意味着不同。在某些情况下,考虑搜索词的同义词可能会更好地帮助用户。在其他情况下,提供在非结构化文本中搜索电子邮件或链接的规定可能对用户有更多帮助。简而言之,即使用户没有提供正确的搜索词,搜索引擎也应该能够猜测用户正在搜索的内容。

在本章中,我们将从用户的角度探讨各种搜索模式,然后进一步了解这些模式是如何产生的。稍后,我们将重点介绍如何借助为 Elasticsearch 开发的内部和外部分析器和插件来解决这些问题。

News search


让我们考虑一个新闻供应商以及他们打开新闻搜索门户的要求。我们被要求为他们创建一个用户友好的搜索。

为此,我们将为每个文档创建一些新闻并围绕它建模数据:

  • 文件 1:

    {
        "Title" : "Doctor opens online free consultation",
        "Content" : "Dr Malhotra offers his free consultation over the internet for patients. The patients can either logon to www.malhotra-help.com or mail him to [email protected]" ,
        "Author" : "Anjali Shankar"
    } 
  • 文件 2:

    {
        "Title" : "Online site for buying groceries",
        "Content" : :"Folks at Factweavers INC have opened at new site called www.buygroceriesonline.com for online purchase of local groceries. The purchase can also be done by mailing to [email protected]" ,
        "Author" : "Anjali shankar"
    } 
  • 文件 3:

    {
        "Title" : "India launches Make In India  campaign",
        "Content" : :"PM of India inaugurates make in India campaign" ,
        "Author" : "Ram Shankar"
    } 

A case-insensitive search


当我们进行搜索时,搜索必须不区分大小写。这意味着即使用户在关键字中使用大写字母进行搜索,他/她的搜索也应该避免字符的大小写,并且匹配关键字与任何大小写。简而言之,在索引和搜索时应该发生以下情况:

 [ "ElasticSearch" , "elasticSearch" ] => "elasticsearch"

为了启用不区分大小写的搜索,我们需要相应地设计我们的分析器。分析器制作完成后,我们只需将其应用于映射中的字段。让我们看看我们如何实现这一点:

  1. 首先,我们需要创建分析器:

    curl -X PUT "http://localhost:9200/news" -d '{
      "analysis": {
        "analyzer": {
          "lowercase": {
            "type": "custom",
            "tokenizer": "standard",
            "filter": [
              "lowercase"
            ]
          }
        }
      }
    }'
  2. 然后,我们需要将分析器应用于必填字段:

    curl -X PUT "http://localhost:9200/news/public/_mapping" -d '{
      "public": {
        "properties": {
          "Title": {
            "type": "string",
            "analyzer": "lowercase"
          },
          "Content": {
            "type": "string",
            "analyzer": "lowercase"
          }
        }
      }
    }'

一旦 您创建了分析器并相应地为其分配了映射中的字段,对该特定字段的任何搜索都将不区分大小写。这是可能的,因为默认情况下,在索引和搜索时使用应用于字段的分析器。

Note

即使在应用分析器之后,一个或多个术语查询也可能不起作用。这是因为,对于这些查询,分析器并未应用于搜索端。

Effective e-mail or URL link search inside text


让我们 在文档的内容字段中搜索邮件地址

{
  "query" : {
    "match" : {
      "content" : "[email protected]"
      }
    }
}

顺便说一句,文档 1 和文档 2 与我们的查询匹配,而不仅仅是文档 1。

让我们看看为什么会发生这种情况以及如何发生:

  • 默认情况下,将标准分析器作为默认分析器

  • 标准分析器将 分解为 malhotra和 gmail.com

  • 标准分析器还会破坏电子邮件 ID 进入 buygroceries 和 gmail.com

  • 这意味着当我们搜索电子邮件 ID ,malhotra 或 gmail.com 都需要匹配才能使文档成为合格的结果

因此,文档 1 和文档 2 都匹配我们的查询,而不仅仅是文档 1。

对于这个问题的 解决方案是使用UAX Email URL tokenizer< id="id264" class="indexterm"> 而不是默认的分词器。此标记器保留电子邮件 ID 和 URL 链接,不会破坏它们。因此我们的搜索工作。

让我们 看看我们如何为此制作分析器:

curl -X PUT "http://localost:9200/news" -d '{
  "analysis": {
    "analyzer": {
      "urlAnalyzer": {
        "type": "custom",
        "tokenizer": "uax_url_email"
      }
    }
  }
}'

最后,我们需要将它应用到我们的领域:

curl -X PUT "http://$hostname:9200/news/public/_mapping" -d '{
  "public": {
    "properties": {
      "Content": {
        "type": "string",
        "analyzer": "urlAnalyzer"
      }
    }
  }
}'

现在,当标记化发生时,它不会破坏基于 @ 的标记。因此,我们以前的条件不会占上风。

Prioritizing a title match over content match


通常要求您对 多个字段进行搜索,但随后,确保某些字段匹配优先于其他匹配以使搜索结果更具相关性也很重要。

如果我们使用多字段搜索,最好使用多搜索查询。在这里,您可以提供多个字段,还可以对某些字段进行优先级排序。一个例子如下:

{
  "query": {
    "multi_match": {
      "query": "white elephant",
      "fields": [
        "Title^10",
        "Content"
      ]
    }
  }
}

在这里,我们搜索 TitleContent 字段并优先考虑 Title Content 字段匹配的字段匹配。 ^10 参数旁边的 Title 字段名称确保匹配 Title 字段的相关性提高了 10 倍。

Terms aggregation giving weird results


让我们考虑 Author 字段中的聚合,以获取每个作者姓名的统计信息:

curl -XPOST  "http://localhost:9200/news/public/_search?pretty&search_type=count" -d '{
    "aggs" : {
      "authors" : {
        "terms" : {
          "field" : "Author"
        }
      }
    }
}'

通过提供 search_type=count,我们确保我们只收到聚合结果而不是命中,或者更确切地说,是前 10 个结果。

我们得到的响应如下:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "authors" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "shankar",
        "doc_count" : 3
      }, {
        "key" : "anjali",
        "doc_count" : 2
      }, {
        "key" : "ram",
        "doc_count" : 1
      } ]
    }
  }
}

在这里,我们看到了意想不到的结果。要了解这里发生了什么,我们需要深入了解 Elasticsearch 的工作原理。 Elasticsearch 建立在名为 Lucene 的强大文本搜索和分析库之上。 Lucene 创建一个反向索引,其中单词/标记 被反向映射到它们出现的文档。

例如,我们举以下两个例子:

Doc-1 = "Elasticsearch is a great tool"
Doc-2 = "Elasticsearch is a search tool"

现在,让我们根据这两个文档创建一个反向索引:

Elasticsearch -> { Doc-1 , Doc-2}
great -> { Doc-1}
tool -> { Doc-1 , Doc-2}

聚合发生在这个破碎的标记集中而不是纯文本中,因此,我们的结果被分解成单词而不是确切的文本。

现在,让我们看看我们可以做些什么来对抗这种行为。

Setting the field as not_analyzed

我们的第一个选项 将绕过 author 字段的分析器。这可以使用映射中的以下设置来实现:

curl -X PUT "http://localhost:9200/news/public/_mapping" -d '{
  "public": {
    "properties": {
      "Author": {
        "type": "string",
        "index": "not_analyzed"
      }
    }
  }
}'

在设置了这个新的映射并索引了所有文档之后,我们可以尝试我们之前的聚合查询。此查询为我们提供以下响应:

{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "authors" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "Anjali Shankar",
        "doc_count" : 1
      }, {
        "key" : "Anjali shankar",
        "doc_count" : 1
      }, {
        "key" : "Ram Shankar",
        "doc_count" : 1
      } ]
    }
  }
}

使用这些 设置,术语聚合进行得很好,但是,如果作者姓名的大小写不同,我们将得到不同的结果。这不是我们所要求的。我们还想覆盖大小写,并希望将结果分组为不区分大小写。

在这种情况下,使用 关键字创建我们自己的分析器会是个好主意。 strong> 分词器。

Using a lowercased analyzer


在之前的尝试中,我们注意到虽然我们能够部分解决问题,但仍然存在 问题。在这种情况下,使用关键字标记器将是一个好方法。关键字标记器允许您在文本到达过滤器之前保持文本原样

Note

not_analyzed 方法相反,关键字标记器方法允许我们对文本应用过滤器,例如小写字母。

现在,让我们看看如何实现这个分析器。

首先,我们需要在创建索引的同时创建我们的分析器:

curl -X PUT "http://localhost:9200/news" -d '{
  "analysis": {
    "analyzer": {
      "flat": {
        "type": "custom",
        "tokenizer": "keyword",
        "filter": "lowercase"
      }
    }
  }
}'

接下来在映射中,我们应该将刚刚制作的平面分析器映射到必填字段:

curl -X PUT "http://localhost:9200/news/public/_mapping" -d '{
  "public": {
    "properties": {
      "Author": {
        "type": "string",
        "analyzer": "flat"
      }
    }
  }
}'

现在,让我们看看 查询的输出:

{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "authors" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "anjali shankar",
        "doc_count" : 2
      }, {
        "key" : "ram shankar",
        "doc_count" : 1
      } ]
    }
  }
}

输出看起来 不错,但是名称的大写字母丢失了。但是,结果对我们来说看起来不错。如果客户端需要,我们可以为结果命名。

Improving the search experience using stemming


词干提取是 将英文单词转换为其基本形式的过程。

部分示例如下所示:

[ running , ran  ] => run
[ laughing ,  laugh , laughed ] => laugh

有了这个,我们的搜索就会好很多。当有人搜索 run 时,所有的文档,无论是包含 running 还是 ran,都将显示为匹配项。

Note

这是可能的,因为我们也在搜索端应用了用于索引的分析器。

为了实现这一点,我们有两种方法:

  • 算法方法:在这种方法中,我们使用基于语言的通用算法将单词转换为其词干或基本形式

  • 基于字典的方法:在这个 方法,我们使用查找机制将单词映射到其基本形式

让我们看看如何实现算法方法以及它的优缺点。

我们的首选是 snowball 算法。这是一种强大的算法,可以使用算法找到给定单词的词干。这意味着它可以智能地删除尾随字符,例如 ing、ed 等。

让我们看看如何实现这个算法并检查它的一些输出:

curl -X PUT "http://localhost:9200/news" -d '{
  "analysis": {
    "analyzer": {
      "stemmerAnalyzer": {
        "type": "custom",
        "tokenizer": "standard",
        "filter": [
          "lowercase",
          "snowball"
        ]
      }
    }
  }
}'

现在,让我们看看这个分析器是如何将文本分解为标记的。为此,我们可以使用分析器 API。分析器 API 用于调试目的,可以将索引分析器作为参数。此分析器应用于有效负载文本,并返回分析的令牌,如以下代码所示:

curl -XPOST 'localhost:9200/news/_analyze?analyzer=stemmerAnalyzer&pretty' -d 'running run'
{
  "tokens" : [ {
    "token" : "run",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "run",
    "start_offset" : 8,
    "end_offset" : 11,
    "type" : "<ALPHANUM>",
    "position" : 2
  } ]
}

在这里,我们可以看到“running”和“run”这两个词映射到它们的基本形式,即run。现在,让我们看一些复杂的例子。让我们看看 snowball 如何处理 insiderinside 文本:

curl -XPOST 'localhost:9200/news/_analyze?analyzer=stemmerAnalyzer&pretty' -d 'insider inside'
{
  "tokens" : [ {
    "token" : "insid",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "insid",
    "start_offset" : 8,
    "end_offset" : 14,
    "type" : "<ALPHANUM>",
    "position" : 2
  } ]
}

现在,“running”这个词的词干很好,但是insider的词干看起来很有趣。只要我们在搜索查询中也使用相同的词,这很好。但是,insiderinside 映射到同一个词干。虽然 insider 看起来与 inside 相似,但它是一个完全不同的术语,具有不同的含义。因此,算法方法在某些情况下可能会失败。

Note

基于雪球算法的词干提取速度很快,但不是 100% 准确。

因此,选择基于字典的方法是一个好主意。 hunspell 过滤器可以用于此目的。

Note

基于 hunspell-dictionary 的词干提取是 100% 准确的,但会影响搜索 的性能,因为必须在所有搜索中进行查找。

A synonym-aware search


Elasticsearch 在搜索时会考虑同义词。这意味着如果您搜索单词“big”,即使该文档中存在单词 big 的任何同义词(例如“large”),也会匹配该文档。

让我们看看我们如何实现一个简单的同义词过滤器以及它是如何工作的:

curl -X PUT "http://localhost:9200/news" -d '{
  "analysis": {
    "filter": {
      "synonym": {
        "type": "synonym",
        "synonyms": [
          "big, large",
          "round, circle"
        ]
      }
    },
    "analyzer": {
      "synonymAnalyzer": {
        "type": "custom",
        "tokenizer": "standard",
        "filter": [
          "lowercase",
          "synonym"
        ]
      }
    }
  }
}'

在这里,我们创建了一个同义词过滤器并在其中提供了一组同义词。 Elasticsearch 没有标准的同义词词典,因此我们需要自己提供。

让我们使用分析器 API 将分析器应用于一些示例文本并检查结果:

curl -XPOST 'localhost:9200/news/_analyze?analyzer=synonymAnalyzer&pretty' -d 'big'
{
  "tokens" : [ {
    "token" : "big",
    "start_offset" : 0,
    "end_offset" : 3,
    "type" : "SYNONYM",
    "position" : 1
  }, {
    "token" : "large",
    "start_offset" : 0,
    "end_offset" : 3,
    "type" : "SYNONYM",
    "position" : 1
  } ]
}

在使用分析 API 检查 输出时,我们可以看到对于具有同义词的特定标记,其所有同义词都添加到该特定位置。这样,我们可以对同义词进行简洁、省时的实现。

The holy box of search


一个神圣的搜索框 是一个文本输入框,您可以在其中输入您想要搜索的所有约束条件。

例如,如果我想在新闻数据库中搜索标题中包含“印度”一词、内容中包含“职业”一词的新闻,日期为 1990 年至 1992 年,作者是 Shiv Shankar,我应该可以通过以下方式快速搜索:

Title:India AND Content:occupation AND date:[1900-01-01 TO 1992-01-01] AND author:"Shiv Shankar" 

对于这种搜索,查询字符串查询是我们选择的武器。

它为我们提供了多种搜索选项,可以写成纯文本。这些特性将在下一节中讨论。

The field search

您可以通过 field:value 表示法指定特定字段。如果您未指定任何字段,则搜索将在 _all 上进行,并且如果您将该字段称为 default_field将采用 query_string_query 下的选项。

The number/date range search

使用 以下表示法,您可以指定数字范围。一些例子如下:

  • 10岁以上:"age:[10 TO *]

  • 10 到 15 岁之间的年龄:" age:[10 TO 15]

  • 年龄不包括 10 岁及以上 10 岁及以下 20 岁:"age:[10 TO 20]

  • 1990 年到 1992 年之间的日期:"date:[1990 TO 1992]

The phrase search

短语搜索用于需要检查字段中连续单词匹配的场景。您可以通过将文本放在双引号中来标记用于短语搜索的文本。例如:

Content:"occupational hazards"

The wildcard search

通配符搜索也可以组合如下:

Content:occup*

The regexp search

还支持类似 Perl 的 正则表达式。例如:

Content: /indi.*/

Boolean operations


现在,让我们看看 结合所有搜索的一些很棒的例子:

Query = "india OR asia AND NOT china"

如您所见,我们使用布尔运算符,例如 ANDORNOT< /code> 并使用它们来标记我们查询中的布尔 条件。

由于查询字符串查询非常强大,并且由于它支持的复杂格式而具有很高的失败几率,因此强烈建议您不要将其暴露给不了解这些操作的最终用户。 simple query string 查询是查询字符串查询的简化版并支持它的一些操作。

Words with similar sounds


如果我们可以映射具有相似发音的单词,例如 forgot 或 forghot,将会提升用户体验。为此,我们可以使用 phonetic 分析器。

语音分析器是一个社区驱动的项目,能够理解具有相似发音的单词。

首先,我们需要 安装语音分析器。您可以在 https://github.com/elastic/elasticsearch 找到代码库-分析-语音

我们可以使用以下命令安装插件:

bin/plugin install elasticsearch/elasticsearch-analysis-phonetic/2.4.2

然后,我们需要从中创建一个分析器:

curl -X PUT "http://localhost:9200/my-index" -d '{
  "index": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "analysis": {
    "filter": {
      "my_metaphone": {
        "type": "phonetic",
        "encoder": "metaphone",
        "replace": false
      }
    },
    "analyzer": {
      "metaphone": {
        "type": "custom",
  "tokenizer" : "standard",
        "filter": "my_metaphone"
      }
    }
  }
}'

在这里,我们正在创建一个名为 my_metaphone 的过滤器,并使用它来创建 metaphone 分析器。 有许多可用于语音过滤的编码器,但在这里,我们选择了变音位。

现在,让我们看看变音位分析器是如何工作的。为此,我们使用 _analyze API 来查看具有相同发音的各种单词的 输出。出于同样的目的,我们选择 aamerika、amerika 和 America 三个词,因为这三个词的发音相同:

curl -XPOST 'localhost:9200/my-index/_analyze?analyzer=metaphone&pretty' -d 'aamerika'
{
  "tokens" : [ {
    "token" : "AMRK",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "aamerika",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "<ALPHANUM>",
    "position" : 1
  } ]
}

我们试试amerika,如下代码所示:

curl -XPOST 'localhost:9200/my-index/_analyze?analyzer=metaphone&pretty' -d 'amerika'

{
  "tokens" : [ {
    "token" : "AMRK",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "amerika",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 1
  } ]
}

最后,我们尝试america如下:

curl -XPOST 'localhost:9200/my-index/_analyze?analyzer=metaphone&pretty' -d 'america'
{
  "tokens" : [ {
    "token" : "AMRK",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "america",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 1
  } ]
}

在这里,我们可以看到这三个词在发音相似的基础上都映射到了同一个标记AMRK

Substring matching


到现在为止,您会注意到只有完全匹配的单词才会被用作结果限定符。如果我想匹配一个不完整的标记或单词怎么办?

例如,当我搜索 titani 时,它应该与 titanium 匹配,因为 titani titanium 的不完整形式。

对于这种 目的,最好使用 EdgeNGram基于分析器。

首先让我们创建分析器:

curl -X PUT "http://localhost:9200/my-index" -d '{
  "index": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "analysis": {
    "filter": {
      "ngram": {
        "type": "edgeNgram",
        "min_gram": 1,
  "max_gram" : 50
      }
    },
    "analyzer": {
      "NGramAnalyzer": {
        "type": "custom",
        "tokenizer" : "standard", 
        "filter": "ngram"
      }
    }
  } 
}'  

在这里,我们创建一个边缘NGram过滤器,它创建子串最小长度为 2,最大长度为 50 的令牌一端的所有令牌。

让我们看看它的输出是什么样子的:

curl -XPOST 'localhost:9200/my-index/_analyze?analyzer=NGramAnalyzer&pretty' -d 'aamerika'
{
  "tokens" : [ {
    "token" : "a",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "aa",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "aam",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "aame",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "aamer",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "aameri",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "aamerik",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "aamerika",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "word",
    "position" : 1
  } ]
}

Summary


我们探索了各种方法来改善用户搜索体验。在所有这些方法中,我们都试图将不同搜索模式的差异映射到用户实际需要的差异。我们使用小写、词干和同义词来实现相同的目的。我们更深入地研究了这个主题并探索了语音和 edgeNGram 分析器,它们提供了类似的功能。

在下一章中,我们将看到如何使用地理信息来获得更好的搜索和评分。