找回密码
 骑士注册

QQ登录

微博登录

搜索
❏ 站外平台:

Linux中国开源社区 技术 查看内容

使用 Docker 和 Elasticsearch 构建一个全文搜索应用程序

作者: Patrick Triest 译者: LCTT qhwdw

| 2018-05-01 09:56   收藏: 5    

如何在超过 500 万篇文章的 Wikipedia 上找到与你研究相关的文章?

如何在超过 20 亿用户的 Facebook 中找到你的朋友(并且还拼错了名字)?

谷歌如何在整个因特网上搜索你的模糊的、充满拼写错误的查询?

在本教程中,我们将带你探索如何配置我们自己的全文搜索应用程序(与上述问题中的系统相比,它的复杂度要小很多)。我们的示例应用程序将提供一个 UI 和 API 去从 100 部经典文学(比如,《彼得·潘》 、  《弗兰肯斯坦》 和  《金银岛》)中搜索完整的文本。

你可以在这里(https://search.patricktriest.com)预览该教程应用的完整版本。

preview webapp

这个应用程序的源代码是 100% 开源的,可以在 GitHub 仓库上找到它们 —— https://github.com/triestpa/guttenberg-search

在应用程序中添加一个快速灵活的全文搜索可能是个挑战。大多数的主流数据库,比如,PostgreSQL 和 MongoDB,由于受其查询和索引结构的限制只能提供一个非常基础的文本搜索功能。为实现高质量的全文搜索,通常的最佳选择是单独的数据存储。Elasticsearch 是一个开源数据存储的领导者,它专门为执行灵活而快速的全文搜索进行了优化。

我们将使用 Docker 去配置我们自己的项目环境和依赖。Docker 是一个容器化引擎,它被 UberSpotifyADP 以及 Paypal 使用。构建容器化应用的一个主要优势是,项目的设置在 Windows、macOS、以及 Linux 上都是相同的 —— 这使我写这个教程快速又简单。如果你还没有使用过 Docker,不用担心,我们接下来将经历完整的项目配置。

我也会使用 Node.js (使用 Koa 框架)和 Vue.js,用它们分别去构建我们自己的搜索 API 和前端 Web 应用程序。

1 - Elasticsearch 是什么?

全文搜索在现代应用程序中是一个有大量需求的特性。搜索也可能是最难的一项特性 —— 许多流行的网站的搜索功能都不合格,要么返回结果太慢,要么找不到精确的结果。通常,这种情况是被底层的数据库所局限:大多数标准的关系型数据库局限于基本的 CONTAINS 或 LIKE SQL 查询上,它仅提供最基本的字符串匹配功能。

我们的搜索应用程序将具备:

  1. 快速 - 搜索结果将快速返回,为用户提供一个良好的体验。
  2. 灵活 - 我们希望能够去修改搜索如何执行的方式,这是为了便于在不同的数据库和用户场景下进行优化。
  3. 容错 - 如果所搜索的内容有拼写错误,我们将仍然会返回相关的结果,而这个结果可能正是用户希望去搜索的结果。
  4. 全文 - 我们不想限制我们的搜索只能与指定的关键字或者标签相匹配 —— 我们希望它可以搜索在我们的数据存储中的任何东西(包括大的文本字段)。

Elastic Search Logo

为了构建一个功能强大的搜索功能,通常最理想的方法是使用一个为全文搜索任务优化过的数据存储。在这里我们使用 Elasticsearch,Elasticsearch 是一个开源的内存中的数据存储,它是用 Java 写的,最初是在 Apache Lucene 库上构建的。

这里有一些来自 Elastic 官方网站 上的 Elasticsearch 真实使用案例。

  • Wikipedia 使用 Elasticsearch 去提供带高亮搜索片断的全文搜索功能,并且提供按类型搜索和 “did-you-mean” 建议。
  • Guardian 使用 Elasticsearch 把社交网络数据和访客日志相结合,为编辑去提供新文章的公众意见的实时反馈。
  • Stack Overflow 将全文搜索和地理查询相结合,并使用 “类似” 的方法去找到相关的查询和回答。
  • GitHub 使用 Elasticsearch 对 1300 亿行代码进行查询。

与 “普通的” 数据库相比,Elasticsearch 有什么不一样的地方?

Elasticsearch 之所以能够提供快速灵活的全文搜索,秘密在于它使用反转索引inverted index

“索引” 是数据库中的一种数据结构,它能够以超快的速度进行数据查询和检索操作。数据库通过存储与表中行相关联的字段来生成索引。在一种可搜索的数据结构(一般是 B 树)中排序索引,在优化过的查询中,数据库能够达到接近线性的时间(比如,“使用 ID=5 查找行”)。

Relational Index

我们可以将数据库索引想像成一个图书馆中老式的卡片式目录 —— 只要你知道书的作者和书名,它就会告诉你书的准确位置。为加速特定字段上的查询速度,数据库表一般有多个索引(比如,在 name 列上的索引可以加速指定名字的查询)。

反转索引本质上是不一样的。每行(或文档)的内容是分开的,并且每个独立的条目(在本案例中是单词)反向指向到包含它的任何文档上。

Inverted Index

这种反转索引数据结构可以使我们非常快地查询到,所有出现 “football” 的文档。通过使用大量优化过的内存中的反转索引,Elasticsearch 可以让我们在存储的数据上,执行一些非常强大的和自定义的全文搜索。

2 - 项目设置

2.0 - Docker

我们在这个项目上使用 Docker 管理环境和依赖。Docker 是个容器引擎,它允许应用程序运行在一个独立的环境中,不会受到来自主机操作系统和本地开发环境的影响。现在,许多公司将它们的大规模 Web 应用程序主要运行在容器架构上。这样将提升灵活性和容器化应用程序组件的可组构性。

Docker Logo

对我来说,使用 Docker 的优势是,它对本教程的作者非常方便,它的本地环境设置量最小,并且跨 Windows、macOS 和 Linux 系统的一致性很好。我们只需要在 Docker 配置文件中定义这些依赖关系,而不是按安装说明分别去安装 Node.js、Elasticsearch 和 Nginx,然后,就可以使用这个配置文件在任何其它地方运行我们的应用程序。而且,因为每个应用程序组件都运行在它自己的独立容器中,它们受本地机器上的其它 “垃圾” 干扰的可能性非常小,因此,在调试问题时,像“它在我这里可以工作!”这类的问题将非常少。

2.1 - 安装 Docker & Docker-Compose

这个项目只依赖 Docker 和 docker-compose,docker-compose 是 Docker 官方支持的一个工具,它用来将定义的多个容器配置 组装  成单一的应用程序栈。

2.2 - 设置项目主目录

为项目创建一个主目录(名为 guttenberg_search)。我们的项目将工作在主目录的以下两个子目录中。

  • /public - 保存前端 Vue.js Web 应用程序。
  • /server - 服务器端 Node.js 源代码。

2.3 - 添加 Docker-Compose 配置

接下来,我们将创建一个 docker-compose.yml 文件来定义我们的应用程序栈中的每个容器。

  1. gs-api - 后端应用程序逻辑使用的 Node.js 容器
  2. gs-frontend - 前端 Web 应用程序使用的 Ngnix 容器。
  3. gs-search - 保存和搜索数据的 Elasticsearch 容器。
version: '3'

services:
  api: # Node.js App
    container_name: gs-api
    build: .
    ports:
      - "3000:3000" # Expose API port
      - "9229:9229" # Expose Node process debug port (disable in production)
    environment: # Set ENV vars
     - NODE_ENV=local
     - ES_HOST=elasticsearch
     - PORT=3000
    volumes: # Attach local book data directory
      - ./books:/usr/src/app/books

  frontend: # Nginx Server For Frontend App
    container_name: gs-frontend
    image: nginx
    volumes: # Serve local "public" dir
      - ./public:/usr/share/nginx/html
    ports:
      - "8080:80" # Forward site to localhost:8080

  elasticsearch: # Elasticsearch Instance
    container_name: gs-search
    image: docker.elastic.co/elasticsearch/elasticsearch:6.1.1
    volumes: # Persist ES data in seperate "esdata" volume
      - esdata:/usr/share/elasticsearch/data
    environment:
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - discovery.type=single-node
    ports: # Expose Elasticsearch ports
      - "9300:9300"
      - "9200:9200"

volumes: # Define seperate volume for Elasticsearch data
  esdata:

这个文件定义了我们全部的应用程序栈 —— 不需要在你的本地系统上安装 Elasticsearch、Node 和 Nginx。每个容器都将端口转发到宿主机系统(localhost)上,以便于我们在宿主机上去访问和调试 Node API、Elasticsearch 实例和前端 Web 应用程序。

2.4 - 添加 Dockerfile

对于 Nginx 和 Elasticsearch,我们使用了官方预构建的镜像,而 Node.js 应用程序需要我们自己去构建。

在应用程序的根目录下定义一个简单的 Dockerfile 配置文件。

# Use Node v8.9.0 LTS
FROM node:carbon

# Setup app working directory
WORKDIR /usr/src/app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install app dependencies
RUN npm install

# Copy sourcecode
COPY . .

# Start app
CMD [ "npm", "start" ]

这个 Docker 配置扩展了官方的 Node.js 镜像、拷贝我们的应用程序源代码、以及在容器内安装 NPM 依赖。

我们也增加了一个 .dockerignore 文件,以防止我们不需要的文件拷贝到容器中。

node_modules/
npm-debug.log
books/
public/

请注意:我们之所以不拷贝 node_modules 目录到我们的容器中 —— 是因为我们要在容器构建过程里面运行 npm install。从宿主机系统拷贝 node_modules 到容器里面可能会引起错误,因为一些包需要为某些操作系统专门构建。比如说,在 macOS 上安装 bcrypt 包,然后尝试将这个模块直接拷贝到一个 Ubuntu 容器上将不能工作,因为 bcyrpt 需要为每个操作系统构建一个特定的二进制文件。

2.5 - 添加基本文件

为了测试我们的配置,我们需要添加一些占位符文件到应用程序目录中。

在 public/index.html 文件中添加如下内容。

<html><body>Hello World From The Frontend Container</body></html>

接下来,在 server/app.js 中添加 Node.js 占位符文件。

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
  ctx.body = 'Hello World From the Backend Container'
})

const port = process.env.PORT || 3000

app.listen(port, err => {
  if (err) console.error(err)
  console.log(`App Listening on Port ${port}`)
})

最后,添加我们的 package.json  Node 应用配置。

{
  "name": "guttenberg-search",
  "version": "0.0.1",
  "description": "Source code for Elasticsearch tutorial using 100 classic open source books.",
  "scripts": {
    "start": "node --inspect=0.0.0.0:9229 server/app.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/triestpa/guttenberg-search.git"
  },
  "author": "patrick.triest@gmail.com",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/triestpa/guttenberg-search/issues"
  },
  "homepage": "https://github.com/triestpa/guttenberg-search#readme",
  "dependencies": {
    "elasticsearch": "13.3.1",
    "joi": "13.0.1",
    "koa": "2.4.1",
    "koa-joi-validate": "0.5.1",
    "koa-router": "7.2.1"
  }
}

这个文件定义了应用程序启动命令和 Node.js 包依赖。

注意:不要运行 npm install —— 当它构建时,依赖会在容器内安装。

2.6 - 测试它的输出

现在一切新绪,我们来测试应用程序的每个组件的输出。从应用程序的主目录运行 docker-compose build,它将构建我们的 Node.js 应用程序容器。

docker build output

接下来,运行 docker-compose up 去启动整个应用程序栈。

docker compose output

这一步可能需要几分钟时间,因为 Docker 要为每个容器去下载基础镜像。以后再次运行,启动应用程序会非常快,因为所需要的镜像已经下载完成了。

在你的浏览器中尝试访问 localhost:8080 —— 你将看到简单的 “Hello World” Web 页面。

frontend sample output

访问 localhost:3000 去验证我们的 Node 服务器,它将返回 “Hello World” 信息。

backend sample output

最后,访问 localhost:9200 去检查 Elasticsearch 运行状态。它将返回类似如下的内容。

{
  "name" : "SLTcfpI",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "iId8e0ZeS_mgh9ALlWQ7-w",
  "version" : {
    "number" : "6.1.1",
    "build_hash" : "bd92e7f",
    "build_date" : "2017-12-17T20:23:25.338Z",
    "build_snapshot" : false,
    "lucene_version" : "7.1.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

如果三个 URL 都显示成功,祝贺你!整个容器栈已经正常运行了,接下来我们进入最有趣的部分。

12345下一页
查看其它分页:

最新评论

我也要发表评论

返回顶部

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。