建立搜索体验是一项艰苦的工作。 乍一看似乎很容易:建立一个搜索栏,将数据放入数据库,然后让用户输入对该数据库的查询。 但是,在数据建模,底层逻辑以及(当然)总体设计和用户体验方面,有很多事情要考虑。
我们将逐步介绍如何使用 Elastic 的开源 Search UI 库构建出色的基于 React 的搜索体验。 大约需要30分钟,然后你便可以将搜索带到需要它的任何应用程序中。
但是首先,是什么使创建搜索如此具有挑战性?
搜索是很难创建的
开发人员在搜索开发中采用许多错误的假设。比如许多相信的假设:
- “知道他们要寻找的客户将按照你期望的方式进行搜索。”
- “你可以编写一个查询解析器,该解析器将始终成功解析查询。”
- “一旦设置,下周搜索将以相同的方式进行。”
- “同义词很容易。”
- ...
得出的结论是,搜索面临许多挑战--而且这些挑战并不简单。 你需要考虑如何管理状态,构建用于过滤,构面,排序,分页,同义词,语言处理等等的组件,等等。 但是,总而言之:
建立出色的搜索需要两个复杂的部分:
(1)搜索引擎,它提供用于增强搜索功能的 API
(2)搜索库,它描绘了搜索体验。
对于搜索引擎,我们将查看 Elastic App Search。
为了获得搜索体验,我们将介绍一个操作系统搜索库:Search UI。
完成后,将如下所示。你也可以在地址上进行在线体验。
搜索引擎: Elastic App Search
App Search 可作为付费托管服务或免费的自助托管发行版提供。 我们将在本教程中使用托管服务,但是请记住,如果你自己托管,你的团队可以免费使用带有基本许可的 Search UI 和 App Search。
计划:将代表有史以来最好的视频游戏的文档编入搜索引擎,然后设计和优化搜索体验以对其进行搜索。
首先,注册14天的试用期-无需信用卡。
创建一个引擎。 你可以选择13种不同的语言。
我们将其命名为 video-games,并将语言设置为英语。
下载最佳视频游戏数据集。如果你由于一些原因不能下载这个数据文件,请到地址 https://github.com/liu-xiao-guo/elastic_app_search 进行下载。
然后使用导入程序将其上传到 App Search。
接下来,单击进入引擎,然后选择 “Credentials” 选项卡。
使用仅对 video-games 引擎具有 Limited Engine Access 的方式创建新的 Public Search Key。
我们可以记下我们刚创建的 Public Search Key 及 Host Indentifier 以便下面之用。
尽管看起来我们目前做的并不多,但我们现在拥有功能全面的搜索引擎,可以使用完善的搜索API来搜索我们的视频游戏数据。
到目前为止,这是我们所做的:
- 创建了一个搜索引擎
- 建立了索引文档
- 创建一个默认的索引 schema
- 创建了一个有限的可以用于外界访问的凭证(credential)
让我们开始使用 “Search UI” 来建立我们的搜索体验。
搜索库:Search UI
我们将使用 create-react-app 脚手架实用程序创建一个 React 应用:
npm install -g create-react-app
create-react-app video-game-search --use-npm
cd video-game-search
在此基础上,我们将安装 Search UI 和 App Search 连接器:
npm install --save @elastic/react-search-ui @elastic/search-ui-app-search-connector
并以开发模式启动该应用程序:
npm start
在你喜欢的文本编辑器中打开 src/App.js
我们将从一些样板代码开始,注意评论部分!
// Step #1, import statements
import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";
import "@elastic/react-search-ui-views/lib/styles/styles.css";
// Step #2, The connector
const connector = new AppSearchAPIConnector({
searchKey: "[YOUR_SEARCH_KEY]",
engineName: "video-games",
hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});
// Step #3: Configuration options
const configurationOptions = {
apiConnector: connector
// Let's fill this in together.
};
// Step #4, SearchProvider: The finishing touches
export default function App() {
return (
<SearchProvider config={configurationOptions}>
<div className="App">
<Layout
// Let's fill this in together.
/>
</div>
</SearchProvider>
);
}
Step 1: 导入声明
我们需要导入我们的 Search UI 依赖关系和 React。
核心组件,连接器和视图组件包含在三个不同的程序包中:
- @elastic/search-ui-app-search-connector
- @elastic/react-search-ui
- @elastic/react-search-ui-views
继续进行时,我们将详细了解它们
import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";
我们还将为该项目导入默认样式表,这将使我们拥有良好的外观,而无需编写我们自己的 CSS 行:
import "@elastic/react-search-ui-views/lib/styles/styles.css";
Step 2: 连接器
我们有来自 App Search 的 Public Search Key 和 Host Identifier。
是时候让他们工作了!
Search UI 中的连接器对象使用 credentials 连接到 App Search 和超级搜索:
const connector = new AppSearchAPIConnector({
searchKey: "[YOUR_SEARCH_KEY]",
engineName: "video-games",
hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});
搜索用户界面可与任何搜索 API 配合使用。 但是通过连接器可以使搜索 API 正常工作,而无需进行任何更深入的配置。
Step 3: configurationOptions
在深入探讨 configurationOptions 之前,让我们花点时间进行反思。
我们将一组数据导入了搜索引擎。 但是,它是什么样的数据?
我们对数据了解的越多,我们就会越了解如何将数据呈现给搜索者。 这样一来,你便可以了解如何配置搜索体验。
我们来看一个对象,这是该数据集中所有对象中的一个:
{
"id":"final-fantasy-vii-ps-1997",
"name":"Final Fantasy VII",
"year":1997,
"platform":"PS",
"genre":"Role-Playing",
"publisher":"Sony Computer Entertainment",
"global_sales":9.72,
"critic_score":92,
"user_score":9,
"developer":"SquareSoft",
"image_url":"https://r.hswstatic.com/w_907/gif/finalfantasyvii-MAIN.jpg"
}
我们看到它有几个文本字段,例如 name,year,platform 等等,还有一些数字字段,例如 critic_score,global_sales 和 user_score。
如果我们提出三个关键问题,我们将足够了解,以提供扎实的搜索体验:
- 大多数人将如何搜索? 以视频游戏的名称命名。
- 大多数人想要看到的结果是什么? 视频游戏的名称,类型,发行商,得分和平台。
- 大多数人将如何过滤,排序和构面? 按得分,体裁,发布者和平台分类。
然后,我们可以将这些答案转换为我们的 configurationOptions:
const configurationOptions = {
apiConnector: connector,
searchQuery: {
search_fields: {
// 1. Search by name of video game.
name: {}
},
// 2. Results: name, genre, publisher, scores, and platform.
result_fields: {
name: {
// A snippet means that matching search terms will be wrapped in <em> tags.
snippet: {
size: 75, // Limit the snippet to 75 characters.
fallback: true // Fallback to a "raw" result.
}
},
genre: {
snippet: {
size: 50,
fallback: true
}
},
publisher: {
snippet: {
size: 50,
fallback: true
}
},
critic_score: {
// Scores are numeric, so we won't snippet.
raw: {}
},
user_score: {
raw: {}
},
platform: {
snippet: {
size: 50,
fallback: true
}
},
image_url: {
raw: {}
}
},
// 3. Facet by scores, genre, publisher, and platform, which we'll use to build filters later.
facets: {
user_score: {
type: "range",
ranges: [
{ from: 0, to: 5, name: "Not good" },
{ from: 5, to: 7, name: "Not bad" },
{ from: 7, to: 9, name: "Pretty good" },
{ from: 9, to: 10, name: "Must play!" }
]
},
critic_score: {
type: "range",
ranges: [
{ from: 0, to: 50, name: "Not good" },
{ from: 50, to: 70, name: "Not bad" },
{ from: 70, to: 90, name: "Pretty good" },
{ from: 90, to: 100, name: "Must play!" }
]
},
genre: { type: "value", size: 100 },
publisher: { type: "value", size: 100 },
platform: { type: "value", size: 100 }
}
}
};
我们已经将 Search UI 连接到我们的搜索引擎,现在我们有一些选项可以控制我们如何搜索数据,显示结果并探索这些结果。 但是我们需要一些东西来将所有内容绑定到 Search UI 的动态前端组件。
Step 4: SearchProvider
这是统治所有对象的对象。 SearchProvider 是所有其他组件嵌套的地方。
Search UI 提供了一个 Layout 组件,用于绘制典型的搜索布局。 有很深的自定义选项,但我们不会在本教程中介绍。
我们将做两件事:
- 将 configurationOptions 传递给 SearchProvider。
- 将一些结构性构建基块放入 Layout 中,并添加两个基本组件:SearchBox 和 Results。
export default function App() {
return (
<SearchProvider config={configurationOptions}>
<div className="App">
<Layout
header={<SearchBox />}
// titleField is the most prominent field within a result: the result header.
bodyContent={<Results titleField="name" urlField="image_url" />}
/>
</div>
</SearchProvider>
);
}
至此,我们已经在前端建立了基础。 在运行此后端之前,还有一些其他细节需要在后端解决。 我们还应该研究相关性模型,以便针对该项目的独特需求微调搜索。
重新进入搜索平台
App Search 具有强大且完善的搜索引擎功能。 它使曾经复杂的调优变得更加有趣。 只需单击几下,我们便可以进行细粒度的相关性调整和无缝的模式更改。
我们将首先调整 schema 以使其实际运行。
登录到 App Search,输入 video-games 引擎,然后单击 “Manage” 部分下的 “Schema”。
索引的 schema 将被展示出来。 默认情况下,这11个字段中的每一个均被视为文本。
在 configurationOptions 对象中,我们定义了两个范围构面来帮助我们搜索数字:user_score 和 critic_score。 为了使 range facet 按预期工作,字段类型必须为数字(number)。
单击每个字段旁边的下拉菜单,将其更改为数字,然后单击 “Update Types”。引擎会即时重新更新索引。 然后,当我们将构面(facet)组件添加到布局中时,范围过滤器将按预期运行。 现在,进入真正的漂亮东西。
下面的部分是高度相关的
具有三个关键的相关功能:Synonyms,Curations 和 Relevance Tuning。
在边栏中的 “Search Settings” 部分下选择每个功能:
Synonyms
世界各地的人们使用不同的词来形容事物。 同义词可帮助你创建被视为一个或一组相同的术语集。
就 video game 搜索引擎而言,我们知道人们会希望找到 Final Fantasy。 但是也许他们会改用FF。
单击进入同义词,然后选择创建同义词集并输入术语:
单击 Save。 你可以根据需要添加任意多个同义词集。
现在,搜索 FF 与搜索 Final Fantasy 的权重相同。
Curations
Curations 是最最让人喜欢的。 如果有人搜索 Final Fantasy 或 FF,该怎么办? 系列赛中有很多游戏-他们会得到哪些?
默认情况下,前五个结果如下所示:
- Final Fantasy VIII
- Final Fantasy X
- Final Fantasy Tactics
- Final Fantasy IX
- Final Fantasy XIII
这似乎不正确……Final Fantasy VII 是所有游戏中最好的 Final Fantasy 游戏。 而且 Final Fantasy XIII 不是很好! 😜
我们可以做到这一点,以便搜索 Final Fantasy 的人会收到 Final Fantasy VII 作为第一结果吗? 我们可以从搜索结果中删除 Final Fnatasy XIII 吗?
我们可以!
单击 “Curations”,然后输入查询:“Final Fantasy”。
接下来,通过抓住表格最左侧的把手将 “Final FantasyVII” 文档拖到 “Promoted Documents” 部分。 然后单击 “Final Fantasy XIII” 文档上的 “Hide Result” 按钮(那个有一条线穿过眼睛的图标,下图列表中第三个图标):
现在,执行 “Final Fantasy” 或 “FF” 搜索的任何人都将首先看到 “Final Fantasy VII”。
他们根本看不到 Final Fantasy XIII。 哈!
我们可以升级和隐藏许多文档。 我们甚至可以对升级后的文档进行排序,因此我们可以完全控制每个查询顶部显示的内容。
Relevance tuning
单击边栏中的 “Relevance Tuning”。
我们搜索一个文本字段:name 字段。 但是,如果我们有多个文本字段可供人们搜索,例如 name 字段和 description 字段,该怎么办? 我们正在使用的 video game 数据集不包含 description 字段,因此我们假想一些文档以进行仔细考虑。
说我们的文档看起来像这样:
{
"name":"Magical Quest",
"description": "A dangerous journey through caves and such."
},
{
"name":"Dangerous Quest",
"description": "A magical journey filled with magical magic. Highly magic."
}
如果有人想找到游戏 Magical Quest,他们会输入该内容作为查询。 但是第一个结果将是 Dangerous Quest:
为什么? 因为在 “Dangerous” 的 description 中 “Magical” 一词出现了3次,所以搜索引擎不会知道一个字段比另一个字段更重要。 然后,它将使 “Dangerous Quest” 的排名更高。 这就是为什么存在相关性调整的难题。
我们可以选择一个字段,除其他外,还可以增加其相关性的权重:
我们看到,当我们增加权重时,正确的项目 “Magical Quest” 上升到顶部,因为 name 字段变得更重要。 我们需要做的就是将滑块拖动到更高的值,然后单击 “Save”。
现在,我们已经使用 App Search 实现了如下的任务:
- 调整 schema,并将 user_score 和 critic_score 更改为数字字段。
- 微调关联(relevance)模型。
这样就总结出了精美的“仪表板”功能-每个功能都有一个匹配的 API 端点,如果你不是 GUI 的用户,则可以使用它们使程序以编程方式工作。
现在,让我们结束 UI。
最后加工
此时,你的 UI 应该可以正常工作了。 尝试一些查询。 首先要说的是,我们缺少探索结果的工具,例如过滤,分面(facet),排序等,但是搜索有效。 我们需要完善用户界面。
在初始的 src/App.js 文件中,我们导入了三个基本组件:
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
根据我们为配置选项定义的内容,让我们添加更多内容。
导入以下组件将启用UI中缺少的功能:
- PagingInfo:在当前页面上显示信息。
- ResultsPerPage:配置每页上显示多少个结果。
- Paging:浏览不同的页面。
- Facet:以数据类型独有的方式过滤和浏览数据。
- Sort:重新定向给定字段的结果。
import {
PagingInfo,
ResultsPerPage,
Paging,
Facet,
SearchProvider,
Results,
SearchBox,
Sorting
} from "@elastic/react-search-ui";
导入后,可以将组件放置到布局中。
布局组件将页面分为多个部分,可以通过 prop 将组件放置在这些部分中。
它包含以下部分:
- header:搜索框/栏
- bodyContent:结果容器
- sideContent:侧边栏,其中包含构面和排序选项
- bodyHeader:围绕结果的“包装器”,其中包含上下文丰富的信息,例如当前页面和每页结果数
- bodyFooter:用于在页面之间快速导航的分页选项
组件呈现数据。根据我们在 configurationOptions 中提供的搜索设置获取数据。现在,我们将每个组件放置在适当的布局部分中。
例如,我们在 configurationOptions 中描述了五个方面的维度,因此我们将创建五个方面的组件。每个 Facet 组件都将使用“字段”属性作为返回数据的键。
我们将它们与我们的 Sorting 组件一起放在 sideContent 部分中,然后将 Paging,PagingInfo 和 ResultsPerPage 组件放在最适合它们的部分中:
<Layout
header={<SearchBox />}
bodyContent={<Results titleField="name" urlField="image_url" />}
sideContent={
<div>
<Sorting
label={"Sort by"}
sortOptions={[
{
name: "Relevance",
value: "",
direction: ""
},
{
name: "Name",
value: "name",
direction: "asc"
}
]}
/>
<Facet field="user_score" label="User Score" />
<Facet field="critic_score" label="Critic Score" />
<Facet field="genre" label="Genre" />
<Facet field="publisher" label="Publisher" isFilterable={true} />
<Facet field="platform" label="Platform" />
</div>
}
bodyHeader={
<>
<PagingInfo />
<ResultsPerPage />
</>
}
bodyFooter={<Paging />}
/>
现在,让我们看一下本地开发环境中的搜索体验。
好多了! 我们提供了丰富的选项来探索搜索结果。
我们引入了一些额外的好处,例如多种排序选项,并且通过添加单个标志使发布者的面可过滤。 尝试使用空白查询进行搜索并浏览所有选项。
最后,让我们看一下搜索体验的最后一项功能。 这是一个受欢迎的...
自动完成 (Autocomplete)
搜索者喜欢自动完成功能,因为它可以提供即时反馈。 它的建议有两种形式:结果和查询。 取决于哪种口味,搜索者将收到相关结果或可能导致结果的潜在查询。
我们将重点关注自动填充作为一种查询建议形式。
这需要两个快速更改。
首先,我们需要将自动完成功能添加到 configurationOptions 对象中:
const configurationOptions = {
autocompleteQuery: {
suggestions: {
types: {
documents: {
// Which fields to search for suggestions
fields: ["name"]
}
},
// How many suggestions appear
size: 5
}
},
...
};
其次,我们需要根据 SearchBox 启用自动填充功能:
...
<Layout
...
header={<SearchBox autocompleteSuggestions={true} />}
/>
...
是的,就是这样。
尝试搜索-键入时,将显示自动完成查询建议。
总结
现在,我们拥有美观的功能性搜索体验。 而且,我们避免了人们在尝试实施搜索时经常会遇到的一堆陷阱。 30分钟还不错,你不是说吗?你可以在地址进行一个完美的体验。
如果你想进一步动态生成数据集,请参阅文章https://swiftype.com/documentation/app-search/api/documents#create
你可以在如下地址找到这个项目的源码:https://github.com/liu-xiao-guo/swiftype-video-game-search
参考: