如何使用 Elasticsearch 构建可用性搜索 UI
本教程将向您展示如何使用 Elasticsearch 构建可用性搜索。
它将涵盖以下内容
- 如何使用嵌套文档索引可用性数据
- 如何使用 React、Instantsearch 和 Searchkit 构建搜索 UI
在这个例子中,我们假设一个木屋预订网站。
先决条件
- Elasticsearch(最好是 7.x 或更高版本)
设置 Elasticsearch
开始使用 Elasticsearch 最简单的方法是使用 Elastic Cloud (在新标签页中打开) 服务。您也可以使用 Docker (在新标签页中打开) 在本地运行 Elasticsearch。
在本教程中,我们将使用 Docker 在本地运行 Elasticsearch。为了简单起见,我们将禁用安全性。如果需要,您可以启用安全性。
拉取 Elasticsearch Docker 镜像
docker pull docker.elastic.co/elasticsearch/elasticsearch:8.6.2
为 Elastic 创建一个 Docker 网络
docker network create elastic
启动 Elasticsearch
docker run --name elasticsearch --net elastic -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e "xpack.security.enabled=false" docker.elastic.co/elasticsearch/elasticsearch:8.6.2
索引可用性数据
在本教程中,我们将使用 Elasticsearch REST API (在新标签页中打开) 来索引和搜索数据。您可以使用任何 Elasticsearch 客户端 (在新标签页中打开) 来执行相同的操作。
创建索引
我们的数据模型将具有以下结构
- 一个
listing
有多个availability
对象 - 每个
availability
对象都有一个开始日期
、结束日期
、类型
和价格
- 每个
listing
都有许多属性,例如名称
、描述
、类别
等。
我们将使用 嵌套文档 (在新标签页中打开) 来建模此数据。这意味着每个 availability
对象都将作为 listing
文档下的嵌套文档进行索引。
让我们创建一个名为 listings
的索引,并为 listing
文档创建映射
curl --location --request PUT 'https://127.0.0.1:9200/listings' \
--header 'Content-Type: application/json' \
--data-raw '{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"description": {
"type": "text"
},
"categories": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"availability": {
"type": "nested",
"properties": {
"start_date": {
"type": "date"
},
"end_date": {
"type": "date"
},
"type": {
"type": "keyword"
},
"price": {
"type": "float"
}
}
}
}
}
}'
重点
availability
字段的类型为nested
。这意味着每个availability
对象都将作为listing
文档下的嵌套文档进行索引。availability.start_date
和availablity.end_date
字段的数据类型为date
。这使我们能够根据日期范围筛选可用性。availability.type
字段的数据类型为keyword
。这使我们能够生成筛选选项,并根据类型筛选可用性。availability.price
字段的数据类型为float
。这使我们能够根据价格筛选可用性。categories
字段的数据类型为text
,并包含一个keyword
子字段。这使我们能够搜索类别,并将其用作按类别列出商品的筛选条件。
添加文档
让我们向 listings
索引添加几个文档
curl --location --request POST 'https://127.0.0.1:9200/listings/_doc' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Cabin in the woods",
"description": "A cozy cabin in the woods",
"categories": ["cabin", "wood", "nature"],
"availability": [
{
"start_date": "2021-01-01",
"end_date": "2021-01-10",
"type": "nightly",
"price": 100
},
{
"start_date": "2021-01-11",
"end_date": "2021-01-20",
"type": "nightly",
"price": 150
},
{
"start_date": "2021-01-21",
"end_date": "2021-01-31",
"type": "nightly",
"price": 200
}
]
}'
curl --location --request POST 'https://127.0.0.1:9200/listings/_doc' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Cabin in the mountains",
"description": "A cozy cabin in the mountains",
"categories": ["cabin", "mountain", "nature"],
"availability": [
{
"start_date": "2021-01-01",
"end_date": "2021-01-10",
"type": "nightly",
"price": 100
},
{
"start_date": "2021-01-11",
"end_date": "2021-01-20",
"type": "nightly",
"price": 150
},
{
"start_date": "2021-01-21",
"end_date": "2021-01-31",
"type": "nightly",
"price": 200
}
]
}'
构建搜索 UI
我们将使用 React (在新标签页打开)、Next.JS (在新标签页打开)、Instantsearch (在新标签页打开) 和 Searchkit (在新标签页打开) 来构建搜索 UI。
让我们创建一个新的 Next.JS 应用
npx create-next-app searchkit-tutorial
安装 Searchkit 和 Instantsearch
cd searchkit-tutorial
npm install searchkit @searchkit/api @searchkit/instantsearch-client react-instantsearch
更新名为 pages/index.js
的文件并添加以下代码
import React from "react";
import Client from "@searchkit/instantsearch-client";
import { InstantSearch, SearchBox, Hits, RefinementList } from "react-instantsearch";
const searchClient = Client({
url: "/api/search"
});
const App = () => (
<InstantSearch indexName="listings" searchClient={searchClient}>
<SearchBox />
<Hits />
</InstantSearch>
);
export default App;
然后添加一个名为 pages/api/search.js
的新文件并添加以下代码
import Client from "@searchkit/api";
const client = Client({
connection: {
host: "https://127.0.0.1:9200",
// if you are authenticating with api key
// https://searchkit.elastic.ac.cn/docs/guides/setup-elasticsearch#connecting-with-api-key
// apiKey: '###'
// if you are authenticating with username/password
// https://searchkit.elastic.ac.cn/docs/guides/setup-elasticsearch#connecting-with-usernamepassword
// auth: {
// username: "elastic",
// password: "changeme"
// },
},
search_settings: {
search_attributes: ["name", "description"]
},
});
// example API handler for Next.js
export default async function handler(req,res) {
const results = await client.handleRequest(req.body);
res.send(results);
}
最后,运行应用
npm run dev
您应该看到以下搜索 UI
调整搜索属性
让我们调整搜索属性以包含 categories
字段。
更新 pages/api/search.js
文件并添加以下代码
import Client from "@searchkit/api";
const client = Client({
connection: {
host: "https://127.0.0.1:9200"
// if you are authenticating with api key
// https://searchkit.elastic.ac.cn/docs/guides/setup-elasticsearch#connecting-with-api-key
// apiKey: '###'
// if you are authenticating with username/password
// https://searchkit.elastic.ac.cn/docs/guides/setup-elasticsearch#connecting-with-usernamepassword
// auth: {
// username: "elastic",
// password: "changeme"
// },
},
search_settings: {
search_attributes: ["name", "description", "categories"]
},
});
// example API handler for Next.js
export default async function handler(req,res) {
const results = await client.handleRequest(req.body);
res.send(results);
}
现在,当您搜索 cabin
时,您应该会看到以下结果
添加筛选条件
让我们为 categories
和嵌套字段 availabilities.type
添加筛选条件。
更新 pages/index.js
文件并添加以下代码
import React from "react";
import Client from "@searchkit/instantsearch-client";
import { InstantSearch, SearchBox, Hits, RefinementList } from "react-instantsearch";
const searchClient = Client({
url: "/api/search"
});
const App = () => (
<InstantSearch indexName="listings" searchClient={searchClient}>
<SearchBox />
<RefinementList attribute="categories" />
<RangeInput attribute="price" />
<RefinementList attribute="type" />
<Hits />
</InstantSearch>
);
export default App;
然后更新 pages/api/search.js
文件并添加以下代码
import Client from "@searchkit/api";
const client = Client({
connection: {
host: "https://127.0.0.1:9200"
},
search_settings: {
search_attributes: ["name", "description", "categories"],
facet_attributes: [
{ field: "categories.keyword", type: "string", attribute: "categories" },
{ field: "price", type: "numeric", attribute: "price", nestedPath: "availability" },
{ field: "type", type: "string", attribute: "type", nestedPath: "availability" }
]
},
});
// example API handler for Next.js
export default async function handler(req,res) {
const results = await client.handleRequest(req.body);
res.send(results);
}
您应该会看到以下 UI
添加日期筛选
让我们在嵌套字段 availability.start_date
和 availability.end_date
上的搜索 UI 中添加日期范围筛选。
更新 pages/index.js
文件并添加以下代码
import React from "react";
import Client from "@searchkit/instantsearch-client";
import { InstantSearch, SearchBox, Hits, RefinementList, RangeInput, createConnector } from "react-instantsearch";
const searchClient = Client({
url: "/api/search"
});
const defaultAvailabilityDates = ['2021-01-01', '2021-01-10']
const AvailabilityDatesConnector = createConnector({
displayName: 'AvailabilityDates',
getProvidedProps: (props, searchState) => {
return {
availabilityDates: searchState.availabilityDates || defaultAvailabilityDates
}
},
refine: (props, searchState, nextValue) => {
return {
...searchState,
availabilityDates: nextValue
}
},
getSearchParameters(searchParameters, props, searchState) {
const { availabilityDates = defaultAvailabilityDates } = searchState;
return searchParameters.addNumericRefinement('availability.start_date', '<=', (new Date(availabilityDates[0])).getTime()).addNumericRefinement('availability.end_date', '>=', (new Date(availabilityDates[1])).getTime());
},
})
const AvailabilityDates = AvailabilityDatesConnector(({ availabilityDates, refine }) => {
return (
<div>
<input type="date"
value={availabilityDates[0]} onChange={(e) => {
refine([e.target.value, availabilityDates[1]])
}}
></input>
<input type="date"
value={availabilityDates[1]}
onChange={(e) => {
refine([availabilityDates[0], e.target.value])
}}
></input>
</div>
)
})
const App = () => (
<InstantSearch indexName="listings" searchClient={searchClient}>
<SearchBox />
<RefinementList attribute="categories" />
<RangeInput attribute="price" />
<RefinementList attribute="type" />
<AvailabilityDates />
<Hits />
</InstantSearch>
);
export default App;
然后更新 pages/api/search.js
文件并添加以下代码
import Client from "@searchkit/api";
const client = Client({
connection: {
host: "https://127.0.0.1:9200"
},
search_settings: {
search_attributes: ["name", "description", "categories"],
facet_attributes: [
{ field: "categories.keyword", type: "string", attribute: "categories" },
{ field: "price", type: "numeric", attribute: "price", nestedPath: "availability" },
{ field: "type", type: "string", attribute: "type", nestedPath: "availability" }
],
filter_attributes: [
{ field: "start_date", type: "date", attribute: "availability.start_date", nestedPath: "availability" },
{ field: "end_date", type: "date", attribute: "availability.end_date", nestedPath: "availability" }
]
},
});
// example API handler for Next.js
export default async function handler(req,res) {
const results = await client.handleRequest(req.body);
res.send(results);
}
在这个例子中,我们在嵌套字段 availability.start_date
和 availability.end_date
上的搜索 UI 中添加了日期范围筛选作为筛选条件。
您应该会看到以下 UI。默认日期范围为 2021-01-01
到 2021-01-10
,这将返回一个包含与时间跨度匹配的可用性条目的商品列表。
您可以更改日期范围并查看结果变化。
突出显示可用日期
当您按可用日期和价格筛选时,您会匹配多个可用性条目。您可以通过在搜索结果中突出显示它们来显示与筛选条件匹配的可用性条目。
更新 pages/index.js
文件并添加以下代码
import React from "react";
import Client from "@searchkit/instantsearch-client";
import { InstantSearch, SearchBox, Hits, RefinementList, RangeInput, createConnector } from "react-instantsearch";
const searchClient = Client({
url: "/api/search"
});
const defaultAvailabilityDates = ['2021-01-01', '2021-01-10']
const demo = createConnector({
displayName: 'AvailabilityDates',
getProvidedProps: (props, searchState) => {
return {
availabilityDates: searchState.availabilityDates || defaultAvailabilityDates
}
},
refine: (props, searchState, nextValue) => {
return {
...searchState,
availabilityDates: nextValue
}
},
getSearchParameters(searchParameters, props, searchState) {
const { availabilityDates = defaultAvailabilityDates } = searchState;
return searchParameters.addNumericRefinement('availability.start_date', '<=', (new Date(availabilityDates[0])).getTime()).addNumericRefinement('availability.end_date', '>=', (new Date(availabilityDates[1])).getTime());
},
})
const AvailabilityDates = demo(({ availabilityDates, refine }) => {
return (
<div>
<input type="date"
value={availabilityDates[0]} onChange={(e) => {
refine([e.target.value, availabilityDates[1]])
}}
></input>
<input type="date"
value={availabilityDates[1]}
onChange={(e) => {
refine([availabilityDates[0], e.target.value])
}}
></input>
</div>
)
})
const ResultView = ({ hit }) => {
const availabilities = hit.inner_hits?.availability || { hits: { hits: [] }}
return (
<div>
<h2>{hit.name}</h2>
<p>{hit.description}</p>
<p>{hit.categories.join(", ")}</p>
<div>
{availabilities.hits.hits.map((a, i) => (
<div key={i}>
<p>{a._source.start_date} - {a._source.end_date}</p>
<p>{a._source.price}</p>
<p>{a._source.type}</p>
</div>
))}
</div>
</div>
)
}
const App = () => (
<InstantSearch indexName="listings" searchClient={searchClient}>
<SearchBox />
<RefinementList attribute="categories" />
<RangeInput attribute="price" />
<RefinementList attribute="type" />
<AvailabilityDates />
<Hits hitComponent={ResultView} />
</InstantSearch>
);
export default App;
更改摘要
- 我们添加了一个新的组件
ResultView
,用于呈现搜索结果。此组件显示名称、描述、类别以及与筛选条件匹配的可用性条目。 - 我们从商品文档的
inner_hits
属性访问可用性条目。inner_hits
属性在搜索查询与嵌套文档匹配时由 Elasticsearch 填充。
您应该会看到 UI
扩展搜索体验
现在您已经拥有了一个基本的搜索 UI,您可以通过添加更多功能(如排序、分页和查询规则)来扩展搜索体验。
- 样式组件 (在新标签页打开):Instantsearch 有大量您可以与 Searchkit 一起使用的组件。
- 查询规则 (在新标签页打开):查询规则允许您通过向搜索查询添加自定义逻辑来自定义搜索体验。例如,您可以添加一条规则来提升具有与当前日期匹配的可用性条目的商品列表。
- 搜索相关性 (在新标签页打开):通过覆盖默认的有机匹配查询来调整搜索相关性。
- 地理搜索组件 (在新标签页打开):构建基于地图的搜索体验
感谢您的关注!
请记住为 Searchkit (在新标签页打开) 点赞!或访问我们的演示站点 https://searchkit.elastic.ac.cn/demos (在新标签页打开) 以查看更多示例。