怎样在用车软件合集网站中找到最适合自己的应用程序?,

AI 驱动的搜索:6 使用上下文来学习特定领域的语言

本章涵盖

  • 对查询意图进行分类
  • 查询意义消歧
  • 从用户信号中学习相关短语
  • 从用户信号中识别关键短语
  • 从用户信号中学习拼写错误和替代术语变体

在第 5 章中,我们演示了如何生成和利用语义知识图,以及如何将实体、事实和关系明确地提取到知识图中。这两种技术都依赖于导航单个文档中术语之间的语言联系或跨多个文档和上下文的术语的统计共现。我们展示了如何使用知识图来查找相关术语,以及如何将这些相关术语集成到各种查询重写策略中,以提高召回率或精确度,同时实现概念搜索,而不仅仅是基于文本的关键字匹配。

在本章中,我们将更深入地探讨理解查询意图的想法,以及使用不同上下文来解释查询中特定领域术语的细微差别。我们将首先探索查询分类,然后展示如何使用这些分类来消除具有多种潜在含义的查询的歧义。这两种方法都将扩展我们对上一章语义知识图的使用。

虽然这些基于语义知识图的方法非常擅长更好地上下文化和解释查询,但它们仍然依赖于拥有准确代表您的领域的高质量文档。因此,它们解释用户查询的效率取决于查询与搜索内容的重叠程度。

例如,如果您的 75% 的用户正在搜索服装,但您的库存大部分是电影和数字媒体,那么当他们搜索“短片”时,所有结果都是运行时间短的视频(称为“数字短片”) “在电影行业),大多数用户都会对结果感到困惑。鉴于查询日志中的数据,如果“短裤”可以映射到查询信号中最常见的其他相关术语(例如“裤子”、“服装”和“衬衫”),那就更好了。

因此,不仅依靠文档内容来了解​术语和短语之间的关系,而且利用用户生成的信号也是非常有益的。因此,在本章的后半部分,我们将探讨如何从用户信号中学习相关阶段,如何从用户信号中提取关键短语,以及如何从使用信号中识别常见的拼写错误和替代拼写。

通过利用基于内容的方法和真实的用户交互来促进您对特定领域术语的理解,您的搜索引擎将能够更好地理解真实的用户意图并做出适当的反应。

6.1 查询意图分类

查询的目标或意图通常与关键字本身一样重要。在搜索新闻或旅游内容的背景下,与在技术背景下搜索“司机撞车”可能意味着两种截然不同的事情。类似地,在电子商务中搜索特定产品名称或产品 ID 的人可能有查看该非常具体的商品的意图,并且很可能想要购买该商品,而像“厨房电器”这样的一般搜索可能会表明该意图只是浏览结果并研究可能提供哪些类型的产品。

在这两种情况下,构建查询分类器对于确定所发出的查询的一般类型都非常有用。根据领域的不同,此查询上下文可以自动应用(限制文档的类别),或者可以用于修改相关性算法(自动提升特定产品,甚至完全跳过结果页面,直接进入产品页面) )。在本节中,我们将展示如何使用第 5 章中的语义知识图作为传入查询的分类器来构建查询分类器。

K 最近邻分类是一种分类类型,它采用一个数据点(例如查询或术语)并尝试查找向量空间中最相似的前 K 个其他数据点。语义知识图遍历本质上是在图遍历的每个级别上进行k近邻搜索。这意味着,如果我们的文档中存在“类别”或“分类”字段,那么我们实际上可以要求语义知识图“查找与我的起始节点相关性最高的类别”。由于起始节点通常是用户的查询,这意味着我们可以使用语义知识图对查询进行分类。

为了延续上一章的势头,让我们继续在本节和下一节中利用 Stackexchange 数据集,因为我们已经将它们组装成一个语义知识图,可以扩展查询分类(本节)和查询感知消歧(第 6.2 节)。

清单 6.1演示了对不同的关键字集进行搜索,然后返回类别分类。为简单起见,由于我们已经索引了多个不同的 Stack Exchange 类别(科幻、健康、烹饪、devops 等),因此我们将使用这些类别作为我们的分类。让我们为一些查询找到语义最相关的类别。

清单 6.1。利用语义知识图进行查询分类。

def run_query_classification(query,keywords_field="body",classification_field="category",classification_limit=5,min_occurrences=5):    classification_query = {        "params": {            "qf": keywords_field,            "fore": "{!type=$defType qf=$qf v=$q}",            "back": "*:*",            "defType": "edismax",            "rows": 0,            "echoParams": "none",            "omitHeader": "true"        },        "query": query,        "facet": {            "classification":{                "type": "terms",                "field": classification_field,                "sort": { "classification_relatedness": "desc"},                "mincount": min_occurrences,                "limit": classification_limit,                "facet": {                    "classification_relatedness": {                        "type": "func",                        "func": "relatedness($fore,$back)"                    }                }            }        }    }    search_results = requests.post(solr_url + collection + "/select",    json=classification_query).json()    print("Query: " + query)    print("  Classifications: ")    for classification_bucket in search_results<"facets"><    "classification"><"buckets">:        print("    " + str(classification_bucket<"val">) + "  " +        str(classification_bucket<"classification_relatedness"><        "relatedness">))    print("\n")run_query_classification( query="docker", classification_field="category",classification_limit=3 )run_query_classification( query="airplane", classification_field="category",classification_limit=1 )run_query_classification( query="airplane AND crash",classification_field="category", classification_limit=2 )run_query_classification( query="camping", classification_field="category",classification_limit=2 )run_query_classification( query="alien", classification_field="category",classification_limit=1 )run_query_classification( query="passport", classification_field="category",classification_limit=1 )run_query_classification( query="driver", classification_field="category",classification_limit=2 )run_query_classification( query="driver AND taxi",classification_field="category", classification_limit=2 )run_query_classification( query="driver AND install",classification_field="category", classification_limit=2 )

结果:

Query: docker  Classifications:    devops  0.8376Query: airplane  Classifications:    travel  0.20591Query: airplane AND crash  Classifications:    scifi  0.01938    travel  -0.01068Query: camping  Classifications:    outdoors  0.40323    travel  0.10778Query: alien  Classifications:    scifi  0.51953Query: passport  Classifications:    travel  0.73494Query: driver  Classifications:    travel  0.23835    devops  0.04461Query: driver AND taxi  Classifications:    travel  0.1525    scifi  -0.1301Query: driver AND install  Classifications:    devops  0.1661    travel  -0.03103

此请求利用语义知识图,根据查询与每个可用分类(类别字段内)之间的语义相似性的比较来查找前 K 个最近邻居。正如您所看到的,每个查询都根据其与每个分类的语义相似性分配了一个或多个潜在分类。

我们看到每个查询的每个潜在类别分类的分类分数,其中airplane和passport分类为travel、camping分类为outdoors和alien分类为scifi。然而,当我们将airplane查询细化为更具体的查询(例如 )时airplane AND crash,我们看到类别从 变为travel,scifi因为有关飞机失事的文档更有可能出现在scifi文档中而不是travel文档中。

类似地,我们可以看到类似 的单词driver,它可以具有多个多义(歧义)含义,返回两个潜在的分类(travel或devops),但travel在没有提供其他上下文时,类别是明确的选择。然而,当提供额外的上下文时,我们可以看到查询driver AND taxi被适当地分类到travel类别,同时driver AND install被适当地分类到devops类别。

由于语义知识图能够使用任意查询的上下文查找语义关系,这使其成为对任意复杂的传入查询进行动态分类的理想工具。基于此分类,您可以在这些类别上自动应用过滤器,将查询路由到特定类型的算法或登录页面,甚至消除查询中术语的含义。我们将进一步探索应用查询意图来改进第 7 章中的一些实时查询。

查询不仅可以从分类中受益,而且我们刚刚看到了一个不明确术语 ( driver) 的示例,该术语还需要区分其多种含义,以便搜索引擎使用正确的解释。这可以通过在我们的查询中再添加一次图形遍历来完成,我们接下来将介绍这一点。

6.2 查询意义消歧

从查询中解释用户意图的最困难的挑战之一是准确理解每个单词的含义。一词多义或含糊不清的术语问题可能会严重影响您的搜索结果。

例如,如果有人访问您的搜索引擎并搜索术语“司机”,则这可能有许多不同的可能含义。其中一些含义包括:

  1. 车辆操作员(出租车司机)
  2. 使计算机的一部分工作的软件(安装打印机驱动程序或其他设备驱动程序)
  3. 一种高尔夫球杆(挥动发球杆)
  4. 一种工具(即螺丝刀)
  5. 推动努力前进的事物(成功的关键驱动力)

同样,如果有人搜索“服务器”,这可能意味着在餐厅接受订单和服务员的人,或者可能意味着运行某些软件作为服务的计算机。图 6.1 展示了这两种潜在的上下文,以及在每种上下文中可能找到的相关术语的类型。


图 6.1。区分模糊术语“服务器”的多种含义

理想情况下,我们希望我们的搜索引擎能够消除这些词义的歧义,并在每个消除歧义的上下文中生成相关术语的唯一列表。在一个相关术语列表中简单地将多个潜在含义混合在一起是不够的,因为搜索者显然有一个我们应该尝试理解和表示的特定意图。

在 6.1 节中,我们演示了如何使用语义知识图将查询自动分类为一组已知类别。鉴于我们已经知道如何对查询进行分类,因此在查询分类之后添加额外的遍历以将相关术语列表与每个特定查询分类关联起来是很简单的。

换句话说,通过从查询遍历到分类,然后遍历到术语,我们能够生成一个术语列表,描述每个顶级分类中原始查询的上下文解释。

清单 6.2演示了一个函数,它将针对语义知识图执行这种消除歧义的查询。

清单 6.2。消除不同上下文中查询意图的歧义

def run_disambiguation_query(query,keywords_field="body",context_field="category", keywords_limit=10,context_limit=5,min_occurrences=5):    disambiguation_query = {        "params": {            "qf": keywords_field,            "fore": "{!type=$defType qf=$qf v=$q}",            "back": "*:*",            "defType": "edismax",            "rows": 0,            "echoParams": "none",            "omitHeader": "true"        },        "query": query,        "facet": {            "context":{                "type": "terms",                "field": context_field,                "sort": { "context_relatedness": "desc"},                "mincount": min_occurrences,                "limit": context_limit,                "facet": {                    "context_relatedness": {                        "type": "func",                        "func": "relatedness($fore,$back)"                    },                    "keywords": {                        "type": "terms",                        "field": keywords_field,                        "mincount": min_occurrences,                        "limit": keywords_limit,                        "sort": { "keywords_relatedness": "desc"},                        "facet": {                            "keywords_relatedness": {                                "type": "func",                                "func": "relatedness($fore,$back)"                            }                        }                    }                }            }        }    }    search_results = requests.post(solr_url + collection + "/select",    json=disambiguation_query).json()    print("Query: " + query)    for context_bucket in search_results<"facets"><"context"><"buckets">:        print("  Context: " + str(context_bucket<"val">) + "  " +        str(context_bucket<"context_relatedness"><"relatedness">))        print("    Keywords: ")        for keywords_bucket in context_bucket<"keywords"><"buckets">:            print("      " + str(keywords_bucket<"val">) + "  " +            str(keywords_bucket<"keywords_relatedness"><"relatedness">))        print ("\n")

通过首先遍历特定上下文(category字段),然后遍历关键字(body字段),我们可以找到最相关的上下文,然后找到与特定于该上下文的原始查询最相关的术语。从这个清单中你可以看到,一个context字段(category默认的字段)和一个keywords字段(body默认的字段)被用作两层遍历的一部分。对于传入的任何查询,我们首先找到语义最相关的类别,然后在该类别中找到与该类别中原始查询语义最相关的术语。

清单 6.3演示了如何调用此函数,传入三个不同的查询,其中包含我们想要查找不同含义的不明确术语,表 5.1 演示了这三个图遍历的结果。

清单 6.3。对多个查询运行查询消歧。每个消歧上下文(category字段)相对于查询进行评分,每个发现的关键字(body字段)相对于查询和消歧上下文进行评分。

run_disambiguation_query( query="server", context_field="category",keywords_field="body" )run_disambiguation_query( query="driver", context_field="category",keywords_field="body", context_limit=2 )run_disambiguation_query( query="chef", context_field="category",keywords_field="body", context_limit=2 )

清单 6.3中的查询结果可以在表 6.1 - 6.3 中找到。

表 6.1。按查询“服务器”的类别列出上下文相关术语

Query: server



  Context: devops  0.787    Keywords:      server  0.91786      servers  0.69526      docker  0.66753      code  0.65852      configuration  0.60976      deploy  0.60332      nginx  0.5847      jenkins  0.57877      git  0.56514      ssh  0.55581
  Context: scifi  -0.27326    Keywords:      server  0.56847      computer  0.16903      computers  0.16403      servers  0.14156      virtual  0.12126      communicate  0.09928      real  0.098      storage  0.09732      system  0.08375      inside  0.0771
  Context: travel  -0.28334    Keywords:      server  0.74462      tipping  0.47834      tip  0.39491      servers  0.30689      vpn  0.27551      tips  0.19982      restaurant  0.19672      bill  0.16507      wage  0.1555      restaurants  0.15309

表 6.1 显示了查询中语义最相关的类别,然后是每个上下文中该字段中server语义最相关的关键字。body根据数据,我们看到 的类别devops在语义上最相关(正分数为 0.787),而接下来的两个类别都包含显着的负分数(-0.27326scifi和 0.28334 travel)。这表明当有人搜索查询时server,devops 类别绝对是该术语最有可能有意义的类别。

如果我们查看每个类别返回的不同术语列表,我们还会看到出现几个不同的含义。在该类别中,该术语具有devops非常具体的含义,特别关注与管理、构建代码并将其部署到计算机服务器相关的工具。server在该scifi类别中,传达了对服务器的更一般的理解,但仍然与计算相关。另一方面,在旅行类别中,“服务器”一词的压倒性意义与在餐厅工作的人有关,正如我们看到的术语,如 、tipping和restaurant出现bill。有趣的是,一种特殊的计算机服务器,vpn也出现了,因为这是强烈建议人们在旅行时使用的一种服务器,以保护他们的互联网通信。

当使用这些数据实现智能搜索应用程序时,如果您知道用户的上下文与旅行相关,那么使用旅行类别中的特定含义是有意义的。然而,如果没有这种上下文,最好的选择通常是选择语义最相关的类别或用户中最受欢迎的类别。

表 6.2。上下文相关术语按查询“driver”的类别列出

Query: driver


  Context: travel  0.23835    Keywords:      driver  0.91524      drivers  0.68676      taxi  0.6008      car  0.54811      license  0.51488      driving  0.50301      taxis  0.45885      vehicle  0.45044      drive  0.43806      traffic  0.43721
  Context: devops  0.04461    Keywords:      driver  0.71977      ipam  0.70462      aufs  0.63954      overlayfs  0.63954      container_name  0.63523      overlay2  0.56817      cgroup  0.55933      docker  0.54676      compose.yml  0.52032      compose  0.4626

表 6.2 演示了查询“driver”的查询消歧。在这种情况下,有两个相关类别,travel语义相关性最高(得分:0.23835)和devops语义相关性较差。我们可以看到 driver 在每个上下文中都出现了两种截然不同的含义,其中,旅行类别中的 driver 与taxi, car, license, driving, 和相关vehicle,而在devops类别中 driver 与ipam, aufs, overlayfs, 等相关,它们都是不同的各种与计算机相关的驱动程序。

如果有人在您的搜索引擎中搜索该词driver,他们显然不打算查找有关该词 driver 的这两种含义的文档,并且如果您仅通过搜索字符串在搜索结果中包含这两种含义,他们可能会感到困惑driver在你的倒排索引中。有多种方法可以处理查询关键字的多个潜在含义,例如按含义对结果进行分组以突出差异,仅选择最可能的含义,在搜索结果中仔细散布不同的含义以提供多样性,或者为以下内容提供替代查询建议:不同的上下文,但通常这里有意识的选择比只是懒惰地将多个不同的含义混在一起要好得多。通过利用本节演示的查询语义解释,您可以更好地理解用户的意图并提供更相关、上下文化的搜索结果。

作为最后一个示例,表 6.3 演示了查询 的查询消歧chef。

表 6.3。查询“chef”的上下文相关术语按类别列出

Query: chef


  Context: devops  0.4461    Keywords:      chef  0.90443      cookbooks  0.76403      puppet  0.75893      docs.chef.io  0.71064      cookbook  0.69893      ansible  0.64411      www.chef.io  0.614      learn.chef.io  0.61141      default.rb  0.58501      configuration  0.57775
  Context: cooking  0.15151    Keywords:      chef  0.82034      cooking  0.29139      recipe  0.2572      taste  0.21781      restaurant  0.2158      cook  0.20727      ingredients  0.20257      pan  0.18803      recipes  0.18285      fried  0.17033

查询的前两个上下文chef都显示出相当积极的相关性分数,表明这两种含义都是可能的解释。虽然devops上下文的分数 (0.4461) 比cooking上下文 (0.15151) 更高,但在这两个含义之间进行选择时,尽可能考虑用户的上下文仍然很重要。chef在上下文中的含义devops与用于构建和部署服务器的 Chef 配置管理软件相关(相关术语包括puppet、ansible等),而在烹饪上下文中,它指的是准备食物的人(cooking、taste、restaurant、ingredients等)。 )。

有趣的是,Chef 软件在其术语中使用了“食谱”和“食谱”的概念,这些概念最初借用自厨房里厨师的概念,因此,当我们进一步深入了解时,我们甚至可能会看到术语之间的重叠。列表,尽管这些其他术语实际上在我们使用的两个广泛分类中也含糊不清(devops和cooking)

当然,如果您的文档有更细粒度的分类,您可能能够对用户的查询得出更细致的、上下文化的解释,从而使语义知识图在细致的查询解释和扩展方面非常有效。通过将查询分类、术语消歧和查询扩展(请参阅第 5.4.5 节)结合在一起,语义知识图可以在人工智能驱动的搜索引擎中增强特定于领域和高度上下文化的语义搜索功能。当我们将这些技术应用于实时语义搜索用例时,我们将在第 7 章中进一步深入探讨这些技术的使用。

6.3 从查询信号中学习相关短语

在第 5 章和本章到目前为止,您已经了解了如何利用内容作为知识图来发现相关术语、对查询进行分类以及消除术语的细微含义的歧义。虽然这些技术很强大,但它们也完全取决于文档的质量以及它们代表用户查询的程度。在本章的其余部分中,我们将探讨有关您的领域的另一个主要知识来源 - 您的用户查询和信号。在许多用例中,您的用户信号可能会提供与您的内容类似的(如果不是更有用的)解释查询的见解。

作为从真实用户行为学习特定领域术语的起点,让我们考虑一下您的查询日志代表什么。对于搜索引擎的每个查询,查询日志都包含运行搜索的人员的标识符、运行的查询以及查询的时间戳。这意味着,如果单个用户搜索多个术语,您可以将这些搜索分组在一起,并告知术语的输入顺序。

虽然并不总是正确,但一个合理的假设是,如果有人在彼此非常接近的时间跨度内输入两个不同的查询,则第二个查询可能是第一个查询的细化,或者关于相关主题。图 6.1 演示了您可能会在查询日志中找到单个用户的实际搜索序列。


图 6.2。从特定用户的查询日志中进行的典型搜索序列

当查看这些查询时,我们直观地了解到“iphond”是“iphone”的错误拼写,“iphone accesories”是“iphone Accessories”的错误拼写,“iphone”、“粉红色手机壳”和“粉红色” iphone case”都是相关术语。我们将在后面的部分中处理拼写错误,因此我们现在可以将它们视为相关术语。

虽然依赖单个用户的信号来推断两个查询相关是不明智的,但如果您看到许多不同用户输入的相同查询组合,那么可以肯定这些查询是相关的。正如我们在第 5 章中演示的,可以扩展查询以包含相关术语并提高召回率。在第 5.4.5 节中,我们根据在文档集合中找到的相关术语扩展了查询,而在本节中,我们将根据查询信号查找相关术语。

6.3.1 挖掘相关查询的查询日志

在挖掘用户信号以进行相关查询之前,我们首先将信号转换为更简单的格式以进行处理。清单 6.4提供了从通用信号结构到简单结构的转换,该结构将查询术语的每次出现映射到搜索该术语的用户。

清单 6.4。将信号映射到关键字、用户对

#Calculation:signals_collection="signals"signals_opts={"zkhost": "aips-zk", "collection": signals_collection}df = spark.read.format("solr").options(**signals_opts).load()df.createOrReplaceTempView("signals")spark.sql("""select lower(searches.target) as keyword, searches.user as userfrom signals as searcheswhere searches.type='query'""").createOrReplaceTempView('user_searches')#Show Results:spark.sql("""select count(*) from user_searches """).show(1)print("Simplified signals format:")spark.sql("""select * from user_searches """).show(3)

结果:

+--------+|    rows|+--------+|  725459|+--------+Simplified signals format:+----------------+-------+|         keyword|   user|+----------------+-------+|             gps| u79559||surge protectors|u644168||      headphones| u35624|+----------------+-------+only showing top 3 rows

您可以从图 6.4 中看到,显示了超过 725K 次查询,以及发送每个查询的用户。我们的目标最终是找到相关查询,因此下一个处理步骤是找到两个查询均由共享用户提交的所有查询对。换句话说,假设单个用户倾向于搜索相关关键词,那么让我们找出每对关键词在用户中同时出现的次数,因为出现次数越多可能意味着关键词越相关。

清单 6.5显示了每个查询对,其中两个查询均由同一用户搜索,以及搜索这两个查询的用户数量 ( users_cooc)。

清单 6.5。查询的总出现次数和共现次数,按搜索查询的用户数量排序。

#Calculation:spark.sql('''select k1.keyword as keyword1, k2.keyword as keyword2,count(distinct k1.user) users_coocfrom user_searches k1 join user_searches k2 on k1.user = k2.user wherek1.keyword > k2.keywordgroup by k1.keyword, k2.keyword ''').createOrReplaceTempView('keywords_users_cooc')spark.sql('''select keyword ,  count(distinct user) users_occfrom user_searchesgroup by keyword ''').createOrReplaceTempView('keywords_users_oc')#Show Results:spark.sql('''select * from keywords_users_oc order by users_occ desc''').show(10)spark.sql('''select count(1) as keywords_users_cooc fromkeywords_users_cooc''').show()spark.sql('''select * from keywords_users_cooc order by users_cooc desc''').show(10)

结果:

+-----------+---------+|    keyword|users_occ|+-----------+---------+|     lcd tv|     8449||       ipad|     7749||hp touchpad|     7144||  iphone 4s|     4642||   touchpad|     4019||     laptop|     3625||    laptops|     3435||      beats|     3282||       ipod|     3164|| ipod touch|     2992|+-----------+---------+only showing top 10 rows+-------------------+|keywords_users_cooc|+-------------------+|             244876|+-------------------++-------------+---------------+----------+|     keyword1|       keyword2|users_cooc|+-------------+---------------+----------+|green lantern|captain america|        23||    iphone 4s|         iphone|        21||       laptop|      hp laptop|        20||         thor|captain america|        18||   skullcandy|          beats|        17||    iphone 4s|       iphone 4|        17||         bose|          beats|        17||      macbook|            mac|        16||      laptops|         laptop|        16||         thor|  green lantern|        16|+-------------+---------------+----------+only showing top 10 rows

清单 6.5中的第一个结果显示了最多的查询搜索。虽然这些可能是最流行的查询,但它们不一定是最常与其他查询同时出现的查询。清单 6.6中的第二个结果显示了查询对 ( ) 的总数244,876,其中同一用户至少搜索了一次两个查询。

最终结果显示排名靠前的查询对,按搜索这两个查询的用户数量排序。在最上面的结果中,您会注意到iphone 4s、iphone、 和iphone 4高度相关,并且laptop、laptops、 和hp laptop也高度相关。具体来说,iphone是 的更一般搜索iphone 4, 是 的更一般搜索iphone,hp laptop是 的更一般形式laptop,laptops是 的拼写变体。您还会注意到thor、captain america、 和green lantern都相关(漫画超级英雄),以及skullcandy、beats和bose(都与耳机相关)。

然而,您还会注意到,顶部结果仅具有23共同出现的用户,这意味着数据点的数量相当稀疏,并且当我们进一步向下移动列表时,共同出现的依赖将具有挑战性,并且可能包括很多噪音。在下一节中,我们将探索一种沿着不同轴(产品交互)将信号组合在一起的技术,这可以帮助解决这个稀疏问题。

虽然直接将用户的搜索数量聚合为同时出现有助于找到最流行的查询对,但搜索的流行度并不是对查找相关性有用的唯一指标。关键词and和of是高度共现的,还有phones, movies, computers, 和electronics,因为它们都是很多人搜索的通用词。为了也关注术语之间关系的强度,而与它们各自的受欢迎程度无关,我们可以利用一种称为逐点互信息的技术。

逐点互信息 (PMI) 是任意两个事件之间相关性的度量。在自然语言处理的背景下,PMI 告诉我们两个单词因为相关而一起出现的可能性与它们偶然一起出现的可能性。有多个函数可用于计算和标准化 PMI,但我们将使用称为 PMI k的变体,其中k = 2,它比 PMI 更好地保持分数一致,而不管词频如何。

PMI 2的计算公式如下:


图 6.3。数学

在我们的实现中,k1和k2代表我们想要比较的两个不同的关键字。P(k1,k2)表示同一用户搜索两个关键字的频率,而P(k1)和p(k2)分别表示用户仅搜索第一个关键字或第二个关键字的频率。直观上,如果关键字一起出现的频率高于基于它们随机出现在一起的可能性的预期,那么它们将具有更高的 PMI 2分数。分数越高,术语在语义上相关的可能性就越大。

清单 6.6演示了我们的并发查询对数据集上的PMI 2计算。

清单 6.6。用户搜索的 PMI2 计算

#Calculation:spark.sql('''select k1.keyword as k1, k2.keyword as k2, k1_k2.users_cooc, k1.users_occas n_users1,k2.users_occ as n_users2,log(pow(k1_k2.users_cooc,2) / (k1.users_occ*k2.users_occ)) as pmi2from keywords_users_cooc as k1_k2joinkeywords_users_oc as k1 on k1_k2.keyword1= k1.keywordjoinkeywords_users_oc as k2 on k1_k2.keyword2 = k2.keyword''').registerTempTable('user_related_keywords_pmi')#Show Results:spark.sql( '''select * from user_related_keywords_pmi where users_cooc >10 order by pmi2desc''').show(10)

结果:

+-----------------+--------------------+----------+--------+--------+------------------+|               k1|                  k2|users_cooc|n_users1|n_users2|              pmi2|+-----------------+--------------------+----------+--------+--------+------------------+|  iphone 4s cases|      iphone 4 cases|        10|     158|     740|-7.064075033237091||     sony laptops|          hp laptops|         8|     209|     432|-7.251876756849249||otterbox iphone 4|            otterbox|         7|     122|     787|-7.580428995040033||    green lantern|     captain america|        23|     963|    1091|-7.593914965772897||          kenwood|              alpine|        13|     584|     717|-7.815078108504774||      sony laptop|         dell laptop|        10|     620|     451|-7.936016631553724||   wireless mouse|           godfather|         6|     407|     248|-7.938722993151467||       hp laptops|        dell laptops|         6|     432|     269| -8.07961802938984||      mp3 players|        dvd recorder|         6|     334|     365|-8.127519408103081||          quicken|portable dvd players|         6|     281|     434| -8.12788026497804|+-----------------+--------------------+----------+--------+--------+------------------+only showing top 10 rows

清单 6.6中的结果按 PMI 2分数(从最高到最低)排序,并且我们将最小出现阈值设置为 >5 以帮助消除噪声。现在,您可以看到一些结果,例如hp laptops、dell laptops、 和sony laptops显示为相关,以及kenwood和 等品牌alpine。wireless mouse然而,值得注意的是,像withgodfather和quickenwith这样的对中也存在噪声portable dvd players。使用 PMI 的一个需要注意的是,与使用共现相比,少数用户中的少量事件一起出现可能更容易导致噪音,而共现是基于许多共现的假设。

融合共现模型和 PI 2模型优点的一种方法是创建综合分数。这将提供流行度和出现可能性的混合,这应该将两个分数匹配的查询对移动到列表的顶部。清单 6.7演示了将这两种措施混合在一起的一种方法。具体来说,我们采用所有共现分数的排名列表 (r1) 和所有 PMI 2分数的排名列表(r2),并将它们混合在一起以生成综合排名分数,如图 6.4 所示。

清单 6.7。结合共现和 PMI2排名的综合排名分数

text{comp_score}(q1,q2) = \frac{((r1(q1,q2) + r2(q1,q2))/(r1(q1,q2) \timesr2(q1,q2)) )}2

图comp_score6.4 中所示的 或综合排名分数分配高分查询对,其中它们在共现列表 ( r1) 中的排名和 PMI 2列表 ( r2) 中的排名较高,而随着术语在 中进一步向下移动,排名较低。排名列表。最终结果是一个混合排名,考虑了查询的流行度(共现性)和相关性的可能性,尽管它们很流行(PMI 2)。清单 6.7显示了如何comp_score使用我们的查询数据计算 。

清单 6.8。计算综合分数,混合共现和 PMI

#Calculation:spark.sql('''select  *, (r1 + r2 /( r1 * r2))/2 as comp_score from ( select *,   rank() over (partition by 1 order by users_cooc desc )  r1 ,   rank() over (partition by 1 order by pmi2 desc )  r2  from user_related_keywords_pmi ) a  ''').registerTempTable('users_related_keywords_comp_score')#Show Results:spark.sql( '''  select k1, k2, users_cooc, pmi2, r1, r2, comp_score  from users_related_keywords_comp_score  where users_cooc >= 10''').show(20)

结果:

+-------------+---------------+----------+-------------------+---+------+------------------+|           k1|             k2|users_cooc|               pmi2| r1|    r2|        comp_score|+-------------+---------------+----------+-------------------+---+------+------------------+|green lantern|captain america|        23| -7.593914965772897|  1|  8626|               1.0||    iphone 4s|         iphone|        21|-10.216737746029027|  2| 56156|              1.25||       laptop|      hp laptop|        20| -9.132682838345458|  3| 20383|1.6666666666666667||         thor|captain america|        18| -8.483026598234463|  4| 13190|             2.125||         bose|          beats|        17|-10.074222345094169|  5| 51916|               2.6||    iphone 4s|       iphone 4|        17| -10.07559536143275|  5| 51964|               2.6||   skullcandy|          beats|        17|  -9.00066454587719|  5| 18792|               2.6||         thor|  green lantern|        16| -8.593796095512284|  8| 14074|            4.0625||      laptops|         laptop|        16|-10.792204327465662|  8| 80240|            4.0625||      macbook|            mac|        16| -9.891277373272931|  8| 45464|            4.0625||   headphones|   beats by dre|        15|  -9.98923457501079| 11| 49046| 5.545454545454546||  macbook air|        macbook|        15| -9.442537922965805| 11| 26943| 5.545454545454546||  macbook pro|        macbook|        15|  -9.73733746318645| 11| 39448| 5.545454545454546||  macbook pro|    macbook air|        13| -9.207068753875852| 14| 21301| 7.035714285714286||         nook|         kindle|        13| -9.661503425798296| 14| 36232| 7.035714285714286||       ipad 2|           ipad|        13| -11.76529194320276| 14|196829| 7.035714285714286||      kenwood|         alpine|        13| -7.815078108504774| 14|  9502| 7.035714285714286||   ipod touch|           ipad|        13|-11.829117705935245| 14|200871| 7.035714285714286||   skullcandy|     headphones|        12| -9.318865873777165| 19| 23317| 9.526315789473685||      macbook|          apple|        12|-10.465639011826868| 19| 62087| 9.526315789473685|+-------------+---------------+----------+-------------------+---+------+------------------+only showing top 20 rows

总体而言,综合排名得分合理地融合了我们的共现和 PMI 2指标,克服了各自的局限性。清单 6.7中显示的最上面的结果看起来都很合理。然而,我们在本节中已经指出的一个问题是,共现数非常稀疏。具体来说,在超过 700,000 个查询信号中,任何查询对的最高共现性是23forgreen lantern和 的重叠用户captain america,如清单 6.5 所示。

在下一节中,我们将展示一种克服这种稀疏数据问题的方法,其中特定查询对的用户之间缺乏重叠。我们将通过将许多具有相似行为的用户聚集到一个更大的组中来实现这一目标。具体来说,我们将把重点转向与用户查询交互的产品,而不是每个单独的用户。

6.3.2 通过产品交互查找相关查询

1.3.1 节中用于查找相关术语的技术取决于大量用户搜索重叠查询。正如我们所看到的,对于过度700K查询信号,任何查询对的重叠度最高的是23用户。由于数据可能非常稀疏,因此对用户以外的其他数据进行聚合通常是有意义的。

在本节中,我们将展示如何使用相同的技术(利用同现 + PMI 2),但根据产品点击信号进行汇总。

在第 1.3.1 节中,我们专门研究了查询信号,但在本节中,我们希望将每个关键字实际映射到搜索该关键字的用户以及用户在搜索结果中单击的任何产品,这已得到演示清单 6.8中。

清单 6.9。将原始信号映射为关键字、用户、产品对

#Calculation:spark.sql("""select lower(searches.target) as keyword,searches.user as user, clicks.target as productfrom signals as searches right join signals as clicks on searches.query_id= clicks.query_idwhere searches.type='query' and clicks.type = 'click'""").createOrReplaceTempView('keyword_click_product')#1#Show Results:print("Original signals format: ")spark.sql(''' select * from signals where type='query' ''').show(3)print("Simplified signals format: ")spark.sql(''' select * from keyword_click_product ''').show(3)

结果:

Original signals format:+--------------------+-----------+--------------------+-----------+-----+-------+|                  id|   query_id|         signal_time|     target| type|   user|+--------------------+-----------+--------------------+-----------+-----+-------+|00001ba7-b74c-421...|u164451_0_1|2020-02-20 14:24:...|MacBook pro|query|u164451||0001465f-cfe1-427...|u608945_0_1|2019-06-15 18:08:...|        g74|query|u608945||000173d5-f570-485...| u93764_0_1|2019-11-30 19:56:...|Pioneer avh|query| u93764|+--------------------+-----------+--------------------+-----------+-----+-------+only showing top 3 rowsSimplified signals format:+-----------------+-------+------------+|          keyword|   user|     product|+-----------------+-------+------------+|lord of the rings|u100793|794043140617||        subwoofer|u100953|713034050223||         game boy|u100981|841872143378|+-----------------+-------+------------+only showing top 3 rows

清单 6.8中的转换将单独的查询和点击信号组合成具有三个键列的单行:keyword、user和product。使用这些数据,我们现在可以根据独立用户查找相同产品的情况来确定任意两个关键字之间的关系强度。

清单 6.9生成关键字对,以确定它们对于所有关键字对的潜在关系,其中两个关键字都在同一文档的查询中使用。在第 1.3.1 节中查找每个用户的重叠查询背后的直觉是每个用户都可能搜索相关项目,但每个产品也可能被相关查询搜索。这将我们的心理模型从“查找有多少用户搜索了这两个查询”转变为“查找所有用户的这两个查询找到了多少文档”。

清单 6.9中的转换结果包括以下几列:

  • k1, k2:两个可能相关的关键字,因为它们都导致了对同一产品的点击
  • n_users1:搜索并点击某个产品的用户数量,k1该产品在某些用户搜索后也被点击k2
  • n_users2:搜索该产品并点击该产品的用户数量,k2该产品在某些用户搜索后也被点击k1
  • users_cooc: n_users1+ nusers2. k1表示搜索或并访问了其他搜索者访问或 的k2产品的用户总数。k1k2
  • n_productsk1:和的搜索者点击的产品数量k2。

清单 6.10。创建信号视图仅用于处理查询

#Calculation:spark.sql('''select k1.keyword as k1, k2.keyword as k2, sum(p1) n_users1,sum(p2) n_users2,sum(p1+p2) as users_cooc, count(1) n_productsfrom(select keyword, product, count(1) as p1 from keyword_click_product group bykeyword, product) as k1join(select keyword, product, count(1) as p2 from keyword_click_product group bykeyword, product) as k2on k1.product = k2.productwhere k1.keyword > k2.keywordgroup by k1.keyword, k2.keyword''').createOrReplaceTempView('keyword_click_product_cooc')#Show Results:spark.sql('''select count(1) as keyword_click_product_cooc fromkeyword_click_product_cooc''').show()spark.sql('''select * from keyword_click_product_cooc order byn_products desc''').show(20)

结果:

+--------------------------+|keyword_click_product_cooc|+--------------------------+|                   1579710|+--------------------------++--------------+-------------+--------+--------+----------+----------+|            k1|           k2|n_users1|n_users2|users_cooc|n_products|+--------------+-------------+--------+--------+----------+----------+|       laptops|       laptop|    3251|    3345|      6596|       187||       tablets|       tablet|    1510|    1629|      3139|       155||        tablet|         ipad|    1468|    7067|      8535|       146||       tablets|         ipad|    1359|    7048|      8407|       132||       cameras|       camera|     637|     688|      1325|       116||          ipad|        apple|    6706|    1129|      7835|       111||      iphone 4|       iphone|    1313|    1754|      3067|       108||    headphones|  head phones|    1829|     492|      2321|       106||        ipad 2|         ipad|    2736|    6738|      9474|        98||     computers|     computer|     536|     392|       928|        98||iphone 4 cases|iphone 4 case|     648|     810|      1458|        95||        laptop|    computers|    2794|     349|      3143|        94||       netbook|       laptop|    1017|    2887|      3904|        94||       netbook|      laptops|    1018|    2781|      3799|        91||    headphones|    headphone|    1617|     367|      1984|        90||        laptop|           hp|    2078|     749|      2827|        89||        tablet|    computers|    1124|     449|      1573|        89||       laptops|    computers|    2734|     331|      3065|        88||           mac|        apple|    1668|    1218|      2886|        88||     tablet pc|       tablet|     296|    1408|      1704|        87|+--------------+-------------+--------+--------+----------+----------+only showing top 20 rows

和计算是查看整体信号质量的两种不同方法,以确定我们对两个术语users_cooc和相关的置信度。结果按当前排序,您可以看到列表顶部的关系列表非常干净,包含:n_productsk1k2n_products

  • 拼写变体:laptops ⇒laptop ;耳机⇒耳机;ETC。
  • 品牌联想:平板电脑⇒ipad;笔记本电脑 ⇒ 惠普 ; mac ⇒ 苹果 ; ETC。
  • 同义词/别名:上网本 ⇒ 笔记本电脑;平板电脑⇒平板电脑
  • 一般/具体改进:平板电脑⇒ipad;iPhone ⇒ iPhone 4 ;电脑⇒平板电脑;电脑 ⇒ 笔记本电脑

您可以编写自定义的、特定于域的算法来识别其中一些特定类型的关系,例如我们将在第 1.5 节中处理拼写变化。

还可以使用n_users1和n_users2来识别两个关键字中哪一个更受欢迎。例如,在拼写变体的示例中,我们看到比( vs. users)headphones更常用,并且也比( vs. users) 更常见。同样,我们发现它的使用比(相对于用户)更常见。head phone1829492headphone1617367tablettablet pc1408296

虽然我们当前的关键字对列表看起来很干净,但它目前仅代表在导致相同产品的搜索中同时出现的关键字对。了解每个关键词的实际受欢迎程度将有助于更好地了解哪些特定关键词对我们的知识图最重要。清单 6.10根据我们的查询信号计算出最流行的关键字,这些关键字至少产生了一次产品点击。

清单 6.11。计算每个关键词的流行度

#Calculation:spark.sql('''select keyword, count(1) as n_users from keyword_click_product group bykeyword''').registerTempTable('keyword_click_product_oc')#Show Results:spark.sql('''select count(1) as keyword_click_product_oc fromkeyword_click_product_oc''').show()spark.sql('''select * from keyword_click_product_oc order byn_users desc''').show(20)

结果:

+------------------------+|keyword_click_product_oc|+------------------------+|                   13744|+------------------------++------------+-------+|     keyword|n_users|+------------+-------+|        ipad|   7554|| hp touchpad|   4829||      lcd tv|   4606||   iphone 4s|   4585||      laptop|   3554||       beats|   3498||     laptops|   3369||        ipod|   2949||  ipod touch|   2931||      ipad 2|   2842||      kindle|   2833||    touchpad|   2785||   star wars|   2564||      iphone|   2430||beats by dre|   2328||     macbook|   2313||  headphones|   2270||        bose|   2071||         ps3|   2041||         mac|   1851|+------------+-------+only showing top 20 rows

此列表与清单 6.5中的列表相同,但现在不再显示搜索关键字的用户数量,而是显示搜索关键字并点击产品的用户数量。我们将使用它作为 PMI 2计算的查询主列表。

现在,我们的查询对列表和查询流行度是根据查询和产品交互生成的,我们的其余计算(PMI 2和综合分数)与第 1.3.1 节中的完全相同,因此我们将在此处省略它们(它们是包含在笔记本中供您运行)。计算完 PMI 2和最终综合分数后,清单 6.11显示了我们基于产品交互的相关项计算的最终结果。

清单 6.12。基于产品交互的最终相关术语评分

# ...calculate PMI2, per Listing 6.6# ...calculate comp_score, per Listing 6.7#Show Resultsspark.sql( '''  select count(1) product_related_keywords_comp_scores from  product_related_keywords_comp_score''').show()spark.sql( '''  select k1, k2, n_users1, n_users2, pmi2, comp_score  from product_related_keywords_comp_score  order by comp_score asc''').show(20)

结果:

+------------------------------------+|product_related_keywords_comp_scores|+------------------------------------+|                             1579710|+------------------------------------++----------+-----------+--------+--------+------------------+------------------+|        k1|         k2|n_users1|n_users2|              pmi2|        comp_score|+----------+-----------+--------+--------+------------------+------------------+|      ipad|hp touchpad|    7554|    4829|1.2318940540272372|               1.0||    ipad 2|       ipad|    2842|    7554| 1.430517155037946|              1.25||    tablet|       ipad|    1818|    7554|1.6685364924472557|1.6666666666666667||  touchpad|       ipad|    2785|    7554|1.2231908670315748|             2.125||   tablets|       ipad|    1627|    7554|1.7493143317791537|               2.6||     ipad2|       ipad|    1254|    7554|1.9027023623302282|3.0833333333333335||      ipad|      apple|    7554|    1814|1.4995901756327583|3.5714285714285716||  touchpad|hp touchpad|    2785|    4829|1.3943192464710108|            4.0625||      ipad|  hp tablet|    7554|    1421|1.5940745096856273| 4.555555555555555||ipod touch|       ipad|    2931|    7554|0.8634782989267505|              5.05||      ipad|      i pad|    7554|     612| 2.415162433949984| 5.545454545454546||    kindle|       ipad|    2833|    7554| 0.827835342752348| 6.041666666666667||    laptop|       ipad|    3554|    7554|0.5933664189857987| 6.538461538461538||      ipad| apple ipad|    7554|     326|2.9163836526446025| 7.035714285714286||    ipad 2|hp touchpad|    2842|    4829|1.1805849845414993| 7.533333333333333||   laptops|     laptop|    3369|    3554|1.2902371152378296|           8.03125||      ipad|         hp|    7554|    1125| 1.534242656892875| 8.529411764705882||     ipads|       ipad|     254|    7554|3.0147863057446345| 9.027777777777779||      ipad|  htc flyer|    7554|    1834|1.0160007504012176| 9.526315789473685||      ipad|    i pad 2|    7554|     204| 3.180197301966425|            10.025|+----------+-----------+--------+--------+------------------+------------------+only showing top 20 rows

6.11 的结果显示了在较小粒度级别上聚合的好处。通过查看导致点击特定产品的所有查询,查询对列表现在比第 1.3.1 节中的要大得多,其中聚合的查询对细化到各个用户。1,579,710244,876您可以看到,当用户聚合时,现在正在考虑查询对(清单 6.5 )。

此外,您可以看到相关查询包括热门查询的更细粒度的变体(ipad、ipad 2、ipad2、i pad、ipads、i pad 2)。如果将此相关术语发现与其他算法(例如拼写错误检测)相结合,那么像这样的更细粒度的变化将会派上用场,我们将在 1.5 节中介绍这一点。

在上一章的语义知识图技术和本章的查询日志挖掘之间,您现在已经看到了多种用于发现相关术语的技术。然而,在我们将相关术语应用于查询之前,首先能够识别查询中已存在的术语非常重要。在下一节中,我们将介绍如何从查询信号中识别已知短语。

6.4 用户信号中的短语检测

在第 5.3 节中,我们展示了如何利用开放信息提取从文档中提取任意短语和关系。这些提取的短语可能是一座金矿,因为它们有助于定义要在用户查询中查找的重要短语列表。

在 5.3 节中,我们实现了开放信息提取技术,从文本文档构建知识图谱。在此过程中,我们使用自然语言处理 (NLP) 模型从文本中的名词短语中提取实体。虽然这对于发现内容中所有相关的特定领域短语大有帮助,但这种方法存在两个不同的问题:

  1. 它会产生大量噪音:并非潜在的大量文档中的每个名词短语都很重要,并且随着文档数量的增加,识别错误短语(误报)的可能性也会增加。
  2. 它忽略了用户关心的内容:用户兴趣的真正衡量标准是通过他们的搜索内容来传达的。他们可能只对您的内容的子集感兴趣,并且他们可能正在寻找甚至在您的内容中没有很好体现的内容。

在本节中,我们将重点讨论如何从用户信号中识别重要的特定领域短语。

6.4.1 将查询视为实体

从查询中提取实体的最简单方法是将完整查询本身视为实体。在像我们的 RetroTech 电子商务网站这样的用例中,这种方法非常有效,因为许多查询都是实际的产品名称、类别、品牌名称、公司名称或人名(演员、音乐家等)。在这种情况下,大多数流行的查询最终都是可以直接用作短语的实体,而不需要任何特殊的解析。

如果您回顾清单 6.10的输出,您会发现以下最流行的查询(按发出它们的用户数量):

+------------+-------+|     keyword|n_users|+------------+-------+|        ipad|   7554|| hp touchpad|   4829||      lcd tv|   4606||   iphone 4s|   4585||      laptop|   3554||      ...+------------+-------+

其中每一个都代表属于已知实体列表的实体,其中许多是多词短语。在这种情况下,提取实体的最简单方法也是最强大的 - 只需利用查询作为实体列表即可。发出每个查询的唯一用户数量越多,您对将其添加到实体列表的信心就越高。

如果您可以将查询与文档交叉引用以查找两者中重叠的短语,那么这是减少嘈杂查询中潜在误报的一种方法。此外,如果您的文档中有不同的字段(例如产品名称或公司),您可以将查询与这些字段交叉引用,以便为查询中找到的实体分配类型。

最终,根据查询的复杂性,简单地利用最常见的搜索作为关键实体和短语可能是生成已知短语列表以在未来查询中提取的最简单的方法。

6.4.2 从更复杂的查询中提取实体

在某些用例中,查询可能包含更多噪音(布尔结构、高级查询运算符等),因此可能无法直接用作实体。在这些情况下,提取实体的最佳方法可能是重新应用第 5 章中的实体提取策略,但适用于您的查询信号。

我们的搜索引擎开箱即用,将查询解析为单独的关键字,并在倒排索引中查找这些单独的关键字,该索引也包含单独的关键字。例如,查询 fornew york city将自动解释为布尔查询new AND york AND city(或者如果您将默认运算符设置为ORthen new OR york OR city),然后相关性排名算法将对每个关键字单独评分,而不是理解某些单词组合成短语,然后使用就不同的意义而言。

然而,能够从查询中识别和提取特定领域的短语可以实现更准确的查询解释和相关性。我们已经在第 5.3 节中演示了一种从文档中提取特定领域短语的方法,即使用 Spacy NLP 库进行依存解析和额外的名词短语。虽然查询通常太短而无法执行真正的依存关系解析,但仍然可以对查询中任何发现的短语应用某些词性过滤,以限制为名词短语。如果您需要将查询部分分开,您还可以对查询进行标记(请参阅 6.14)并删除查询语法(and,or等),然后再查找要提取的短语。处理应用程序的特定查询模式可能需要一些特定于域的查询解析逻辑,但如果您的查询主要是单个短语或很容易标记为多个短语,则您的查询可能代表要提取和添加的特定于域的短语的最佳来源到你的知识图谱。

6.5 拼写错误和替代表示

我们已经介绍了检测特定领域的短语和查找相关短语,但是相关短语有两个非常重要的子类别,通常需要特殊处理:拼写错误和替代拼写。输入查询时,用户通常会拼写错误的关键字,人们普遍期望人工智能驱动的搜索系统能够理解并正确处理这些拼写错误。

虽然“laptop”的一些一般相关短语可能是“computer”、“netbook”或“tablet”,但拼写错误看起来更像“latop”、“laptok”或“lapptop”。备用拼写在功能上与拼写错误没有什么不同,但当术语存在多个有效变体时就会出现(例如“specialized”与“specialized”或“cybersecurity”与“cyber security”)。在拼写错误和替代拼写的情况下,最终目标通常是将不太常见的变体规范化为更常见的规范形式,然后搜索规范版本。

拼写检查可以通过多种方式实现。在本节中,我们将介绍大多数搜索引擎中都可以找到的开箱即用的基于文档的拼写检查,并且我们还将展示如何挖掘用户信号以根据实际情况找到更多微调的拼写更正。用户与搜索引擎的交互。

6.5.1 从文档中学习拼写更正

大多数搜索引擎都包含某种形式的开箱即用的拼写检查功能,该功能基于集合文档中找到的术语。例如,Apache Solr 提供了基于文件的拼写检查组件、基于字典的拼写检查组件和基于索引的拼写检查组件。基于文件的拼写检查器需要组装可以进行拼写更正的术语列表,基于字典的拼写检查组件可以从索引中的字段构建要进行拼写更正的术语列表,并且基于索引的拼写检查器可以使用主索引上的字段直接进行拼写检查,而无需构建单独的拼写检查索引。此外,如果有人建立了离线拼写纠正列表,

Elasticsearch 和 OpenSearch 具有一些类似的拼写检查功能,甚至允许传入特定上下文以将拼写建议的范围细化到特定类别或地理位置。

虽然我们鼓励您测试这些开箱即用的拼写检查算法,但不幸的是,它们都面临一个主要问题:缺乏用户上下文。具体来说,只要搜索到的关键字在索引中出现的次数未达到最小次数,拼写检查组件就会开始查找索引中“偏离最小字符数”的所有术语,然后返回索引中与该条件匹配的最流行的关键字。清单 6.12显示了 Solr 开箱即用的基于索引的拼写检查配置的不足之处。

清单 6.13。对文档使用开箱即用的拼写更正。

collection="products"query="moden"request = {    "params": {        "q.op": "and",        "rows": 0,        "indent": "on"    },    "query": query,}search_results = requests.post(solr_url + collection + "/spell",json=request).json()print(json.dumps(search_results<"spellcheck"><"collations">, indent=4))

结果:

<    "collation",    {        "collationQuery": "modern",        "hits": 42,        "misspellingsAndCorrections": <            "moden",            "modern"        >    },    "collation",    {        "collationQuery": "model",        "hits": 40,        "misspellingsAndCorrections": <            "moden",            "model"        >    },    "collation",    {        "collationQuery": "modem",        "hits": 29,        "misspellingsAndCorrections": <            "moden",            "modem"        >    },    "collation",    {        "collationQuery": "modena",        "hits": 1,        "misspellingsAndCorrections": <            "moden",            "modena"        >    },    "collation",    {        "collationQuery": "modes",        "hits": 1,        "misspellingsAndCorrections": <            "moden",            "modes"        >    }>

在清单 6.12中,您可以看到一个用户查询moden. modern拼写检查器会返回、model和 的拼写更正建议modem,以及仅出现在单个文档中且我们将忽略的其他一些建议。由于我们的收藏是科技产品,因此很明显其中哪一个可能是最好的拼写更正:它是modem。事实上,用户不太可能有意搜索modern或model作为独立查询,因为这些都是相当通用的术语,只有在包含其他单词的上下文中才有意义。

基于内容的索引无法轻松区分最终用户不太可能实际搜索modern或model,尽管这些术语在搜索的文档中多次出现。因此,虽然基于内容的拼写检查器在许多情况下可以很好地工作,但从用户的查询行为中学习拼写更正通常可以更准确。

6.5.2 从用户信号中学习拼写更正

回到我们第 6.3 节的核心论点,即用户倾向于搜索相关查询,直到找到预期结果,很明显,拼写错误特定查询并收到错误结果的用户会尝试纠正他们的查询。

我们已经知道如何查找相关短语(第 6.3 节),但在本节中,我们将介绍如何专门区分拼写错误和用户信号。这项任务主要归结为两个目标:

  1. 查找具有相似拼写的术语。
  2. 找出哪个术语是正确的拼写,哪个是拼写错误的变体。

对于此任务,我们将仅依赖查询信号,但执行一些预先规范化以使查询分析不区分大小写并进行限制以避免信号垃圾邮件。您可以在第 8.2-8.3 节中阅读有关此类信号归一化的更多信息。清单 6.13显示了这个查询来获取我们的标准化查询信号。

清单 6.14。从用户信号中获取所有查询,删除每个用户的重复项并忽略大小写

query_signals = spark.sql("""  select lower(searches.target) as keyword, #1  searches.user as user  from signals as searches where searches.type='query'  group by keyword, user""" #2  ).collect()

出于本节的目的,我们将假设查询包含多个不同的关键字,并且我们希望将每个单独的关键字视为潜在的拼写变体。这将允许在未来的查询中找到并替换单个术语,而不是将整个查询视为单个短语。它还允许我们丢弃某些可能是噪音的术语,例如停用词或独立数字。

清单 6.14演示了对每个查询进行标记以生成我们可以进行进一步分析的单词列表的过程。

清单 6.15。对每个查询中的术语进行标记和过滤,以生成清理后的单词列表

stop_words = set(stopwords.words('english')) #1word_list = defaultdict(int)for row in query_signals:    query = row<"keyword">    tokenizer = RegexpTokenizer(r'\w+') #2    tokens   = tokenizer.tokenize(query) #2    for token in tokens:        if token not in stop_words and len(token) > 3 and not        token.isdigit():  #3            word_list += 1 #4

清理完令牌列表后,下一步就是确定高出现率令牌与不频繁出现令牌。由于拼写错误出现的频率相对较低,而正确拼写的出现频率较高,因此我们将使用相对出现次数来确定哪个版本是最可能的规范拼写以及哪些变体是拼写错误。

为了确保我们的拼写更正列表尽可能干净,我们将为流行术语设置一些阈值,并为更有可能拼写错误的低出现率术语设置一些阈值。由于某些集合可能包含数百个文档,而其他集合可能包含数百万个文档,因此我们不能只查看这些阈值的绝对数字,因此我们将使用分位数。清单 6.15显示了 0.1 到 0.9 之间每个四分位数的计算。您可以将分位数想象成一条钟形曲线,其中 0.5 显示某个术语的整体搜索次数中位数,0.2 显示该术语的搜索次数,而 20% 的其他术语出现频率较低。

清单 6.16。从用户信号中获取所有查询,删除每个用户的重复项

quantiles_to_check = <0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9>quantile_values = np.quantile(np.array(list(word_list.values())),quantiles_to_check)quantiles = dict(zip(quantiles_to_check, quantile_values))quantiles

结果:

{0.1: 5.0, 0.2: 6.0, 0.3: 8.0, 0.4: 12.0, 0.5: 16.0, 0.6: 25.0, 0.7: 47.0, 0.8: 142.20000000000027, 0.9: 333.2000000000007}

在这里,我们看到 80% 的术语搜索次数为 142.2 次或更少(因此 20% 的术语搜索次数为 142.2 次或更多)。同样,只有 20% 的术语搜索次数为 6.0 次或更少。使用帕累托原则,我们假设大多数拼写错误属于搜索次数最少的 20% 的术语,而大多数最重要的术语属于搜索查询的前 20%。如果您想要更高的精度(仅对高值术语进行拼写检查,并且仅在误报概率较低的情况下进行纠正),您可以将拼写错误的术语推至 0.1 四分位数,将拼写正确的术语推至 0.9 四分位数,或者您可以另一个方向是尝试生成更大的拼写错误列表,并具有更高的误报变化。

在清单 6.16中,我们将术语划分为存储桶,将低频术语分配给“misspell_candidates”存储桶,将高数量术语分配给“ Correction_candidates”存储桶。当足够多的用户搜索拼写错误候选者和校正候选者时,这些存储桶将成为寻找高质量拼写更正的起点。

清单 6.17。从用户信号中获取所有查询,删除每个用户的重复项

misspell_candidates = <>correction_candidates = <>misspell_counts = <>correction_counts = <>misspell_length = <>correction_length = <>misspell_initial = <>correction_initial = <>for k, v in word_list.items():    if v <= quantiles<0.2> : #1        misspell_candidates.append(k) #1        misspell_counts.append(v) #2        misspell_length.append(len(k)) #3        misspell_initial.append(k<0>) #4    if v >= quantiles<0.8>: #5        correction_candidates.append(k) #5        correction_counts.append(v) #5        correction_length.append(len(k)) #5        correction_initial.append(k<0>) #5

为了有效地比较所有的misspell_candidates和correction_candidates,我们首先将它们加载到清单 6.17中的数据帧中。您可以想象这correction_candidates是最流行搜索术语的原始列表。更有趣的列表是列表misspell_candidates,在初始采样时(也在清单 6.14中)应该可以很好地了解有多少不常搜索的术语实际上代表了拼写错误。

清单 6.18。从用户信号中获取所有查询,删除每个用户的重复项

misspell_candidates_df = pd.DataFrame({  "misspell":misspell_candidates,  "misspell_counts":misspell_counts,  "misspell_length":misspell_length,  "initial":misspell_initial})correction_candidates_df = pd.DataFrame({  "correction":correction_candidates,  "correction_counts":correction_counts,  "correction_length":correction_length,  "initial":correction_initial})misspell_candidates_df.head(10)

结果

    misspell   misspell_counts     misspell_length         initial    --------    ---------------     ---------------     -------0       misery      5                   6                       m1       mute        6                   4                       m2       math        6                   4                       m3       singin      6                   6                       s4       thirteen        5                       8                       t5       nintendogs      6                       10                      n6       livewire        6                       8                       l7       viewer      6                   6                       v8       gorillaz        6                       8                       g9       tosheba     6                   7                       t

您可以在清单 6.17 中看到,在 10 个拼写错误候选样本中,至少有 3 个(“singin”、“nintendogs”和“tosheba”)明显是拼写错误。您还可以看到,术语的 misspell_length 有所不同,并且术语越长,越有可能包含多个错误字符。例如,nintendogs是 的拼写错误nintendo,因此会相差两个字符,而如果“singin”和“tosheba”都少于 8 个字符,则仅相差一个字符。

当我们将拼写错误候选者与更正候选者进行比较时,在决定允许多少个字符差异(或编辑距离)时考虑术语长度非常重要。清单 6.18显示了一个good_match函数,我们将使用该函数作为一般启发式来确定术语匹配可以偏离多少编辑距离,同时仍然考虑拼写错误和校正候选的可能排列。

清单 6.19。从用户信号中获取所有查询,删除每个用户的重复项

def good_match(len1, len2, edit_dist): #allow longer words have moreedit distance    match = 0    min_length = min(len1, len2)    if min_length < 8:        if edit_dist == 1: match = 1    elif min_length < 11:        if edit_dist <= 2: match = 1    else:        if edit_dist == 3: match = 1    return match

将 misspell_candidates 和 Correction_candidates 加载到数据帧中并且我们的good_match函数准备就绪后,是时候实际生成拼写更正列表了。就像第 1.6.1 节中根据我们的文档集合中的编辑距离和术语出现次数生成拼写更正一样,在清单 6.19中,我们将根据查询日志中的编辑距离和术语出现次数生成拼写更正。

清单 6.20。生成映射到更正的拼写错误的最终列表

matches_candidates = pd.merge(misspell_candidates_df, correction_candidates_df, on="initial") #1matches_candidates<"edit_dist"> = matches_candidates.apply(lambda row:nltk.edit_distance(row.misspell,row.correction), axis=1) #2matches_candidates<"good_match"> = matches_candidates.apply(lambda row:good_match(row.misspell_length, row.correction_length, row.edit_dist),axis=1) #3matches = matches_candidates == 1>.drop(<"initial","good_match">,axis=1)matches_final.sort_values(by=<'correction_counts'>, ascending=)<<"misspell", "correction", "misspell_counts", "correction_counts","edit_dist">>.head(20) #4

结果:

       misspell        correction          misspell_counts     correction_counts       edit_dist81      latop       laptop              5                   14258                   1156     touxhpad        touchpad            5               11578                   121      cape        case                5                   7596                    185      loptops     laptops             5                   5628                    111      bluetooh        bluetooth           5               4499                    1178     wats        wars                5                   4196                    177      kimdle      kindle              5                   4159                    196      moden       modem               5                   3598                    1106     phono       phone               5                   3073                    1111     poker       power               5                   2993                    1159     transfomer      transformers    5                   2889                    295      mircosoft       microsoft           5               2265                    2141     sistem      system              5                   2185                    197      motorla     motorola        5               2022                    112      blur        blue                5                   1916                    1177     walls       wall                5                   1818                    1167     turttle     turtle              5                   1583                    1135     share       sharp               5                   1531                    184      logictech       logitech            5               1488                    1151     teater      theater             5                   1430                    1

正如您所看到的,我们现在有一个基于用户信号的相对干净的拼写更正列表,而不仅仅是文档中出现的术语。事实上,我们之前的查询moden正确地映射到了modem不太可能的搜索词,就像model我们modern在清单 6.12中基于文档的拼写更正中看到的那样。

还有许多其他方法可以用来创建拼写纠正模型。如果您想从文档生成多术语拼写更正,您可以生成二元组和三元组,并对连续术语出现的概率执行链式贝叶斯分析。同样,要从查询信号生成多术语拼写纠正,您可以简单地选择在应用本节中的算法之前不对查询进行标记,然后您将对整个查询而不是单个术语进行拼写纠正。例如,如果您将清单 6.14替换为清单 6.20,那么您将获得整个查询的拼写更正,而不仅仅是单个关键字。

清单 6.21。删除查询的标记化,并查找整个查询字符串的拼写错误

stop_words = set(stopwords.words('english'))word_list = defaultdict(int)for row in query_signals:    query = row<"keyword">.strip()    if query not in stop_words and len(query) > 3 and not query.isdigit():        word_list += 1#run Listing 16.12-16.15 again......matches_final.sort_values(by=<'correction_counts'>, ascending=)<<"misspell", "correction", "misspell_counts", "correction_counts","edit_dist">>.head(20)

结果:

                   misspell        correction      misspell_counts              correction_counts     edit_dist181                 ipad.               ipad            6                       7749             1154                 hp touchpad 32      hp touchpad         57144                    3155                 hp toucpad      hp touchpad     67144                    1153                 hp tochpad      hp touchpad     67144                    1190                 iphone s4       iphone 4s       54642                    2193                 iphone4 s       iphone 4s       54642                    2194                 iphones 4s      iphone 4s       54642                    1412                 touchpaf        touchpad        54019                    1406                 tochpad             touchpad            64019                    1407                 toichpad        touchpad        64019                    1229                 latop               laptop          53625                    1228                 laptopa             laptops         63435                    1237                 loptops             laptops         53435                    1205                 ipods touch     ipod touch      62992                    1204                 ipod tuch       ipod touch      62992                    1165                 i pod tuch      ipod touch      52992                    2173                 ipad 2              ipad 2          62807                    1215                 kimdle              kindle          52716                    1206                 ipone               iphone          62599                    1192                 iphone3             iphone          62599                    1

请注意,单词词基本相同,但现在多词查询也已进行拼写更正。这是标准化产品名称的好方法,例如iphone4 s,iphone4 s并且iphone s4所有名称都正确映射到规范iphone 4s。请注意,在某些情况下,这可能是一个有损过程,因为hp touchpad 32映射到hp touchpad和iphone3映射到iphone。因此,根据您的用例,您可能会发现仅拼写正确的单个术语或以其他方式在函数中good_match对品牌变体进行特殊处理以确保拼写检查代码不会错误地删除相关查询上下文是有益的。

6.6 将所有内容整合在一起

在本章中,我们更深入地理解特定领域语言的上下文和含义。我们展示了如何使用语义知识图对查询进行分类,并根据上下文消除具有不同或微妙含义的术语的歧义。我们还探讨了如何从用户信号中挖掘关系,这通常为理解用户提供了比单独查看文档更好的上下文。我们还展示了如何从查询信号中提取短语、拼写错误和替代拼写,从而能够直接从用户那里学习特定领域的术语,而不是仅从文档中学习。

此时,您应该有信心能够从文档或使用信号中学习特定领域的短语,从文档或用户信号中学习相关短语,对可用内容的查询进行分类,并根据查询消除术语含义的歧义分类。现在,这些技术中的每一种都是您工具箱中用于解释查询意图的关键工具。

不过,我们的目标不仅仅是组装一个大型工具箱。我们的目标是在适当的情况下利用这些工具中的每一个来构建端到端语义搜索层。这意味着我们需要将已知短语建模到我们的知识图中,以便能够从传入的查询中实时提取这些短语,处理拼写错误、查询分类和动态消除传入术语的歧义,并最终生成重写的查询使用我们每种人工智能驱动的搜索技术的搜索索引。在下一章中,我们将向您展示如何将这些技术中的每一种组装到一个工作语义搜索系统中,该系统旨在最好地解释和建模查询意图。

6.7 总结

  • 使用语义知识图对查询进行分类可以帮助解释查询意图并改进查询路由和过滤
  • 查询意义消歧可以帮助确定对用户查询的细微差别的理解,特别是对于在不同上下文中具有显着不同含义的术语。
  • 除了从文档中学习之外,还可以从用户信号中学习特定领域的短语和相关短语。
  • 拼写错误和替代拼写变体可以从文档和用户信号中学习,基于文档的方法更加稳健,但基于用户信号的方法可以更好地表示用户意图。

2023-12-28

2023-12-28