文档
教程
可用性搜索 UI

如何使用 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_dateavailablity.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

Image description

调整搜索属性

让我们调整搜索属性以包含 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 时,您应该会看到以下结果

Image description

添加筛选条件

让我们为 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

Image description

添加日期筛选

让我们在嵌套字段 availability.start_dateavailability.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_dateavailability.end_date 上的搜索 UI 中添加了日期范围筛选作为筛选条件。

您应该会看到以下 UI。默认日期范围为 2021-01-012021-01-10,这将返回一个包含与时间跨度匹配的可用性条目的商品列表。

Image description

您可以更改日期范围并查看结果变化。

Image description

突出显示可用日期

当您按可用日期和价格筛选时,您会匹配多个可用性条目。您可以通过在搜索结果中突出显示它们来显示与筛选条件匹配的可用性条目。

更新 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

Image description

扩展搜索体验

现在您已经拥有了一个基本的搜索 UI,您可以通过添加更多功能(如排序、分页和查询规则)来扩展搜索体验。

感谢您的关注!

请记住为 Searchkit (在新标签页打开) 点赞!或访问我们的演示站点 https://searchkit.elastic.ac.cn/demos (在新标签页打开) 以查看更多示例。


Apache 2.0 2024 © Joseph McElroy。
需要帮助?加入 Discord