使用 Docker 和 Elasticsearch 构建一个全文搜索应用程序
| 2018-05-01 09:56 收藏: 5
5 - 搜索
现在,Elasticsearch 中已经有了 100 本书了(大约有 230000 个段落),现在我们尝试搜索查询。
5.0 - 简单的 HTTP 查询
首先,我们使用 Elasticsearch 的 HTTP API 对它进行直接查询。
在你的浏览器上访问这个 URL - http://localhost:9200/library/_search?q=text:Java&pretty
在这里,我们将执行一个极简的全文搜索,在我们的图书馆的书中查找 “Java” 这个词。
你将看到类似于下面的一个 JSON 格式的响应。
{
"took" : 11,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 13,
"max_score" : 14.259304,
"hits" : [
{
"_index" : "library",
"_type" : "novel",
"_id" : "p_GwFWEBaZvLlaAUdQgV",
"_score" : 14.259304,
"_source" : {
"author" : "Charles Darwin",
"title" : "On the Origin of Species",
"location" : 1080,
"text" : "Java, plants of, 375."
}
},
{
"_index" : "library",
"_type" : "novel",
"_id" : "wfKwFWEBaZvLlaAUkjfk",
"_score" : 10.186235,
"_source" : {
"author" : "Edgar Allan Poe",
"title" : "The Works of Edgar Allan Poe",
"location" : 827,
"text" : "After many years spent in foreign travel, I sailed in the year 18-- , from the port of Batavia, in the rich and populous island of Java, on a voyage to the Archipelago of the Sunda islands. I went as passenger--having no other inducement than a kind of nervous restlessness which haunted me as a fiend."
}
},
...
]
}
}
用 Elasticseach 的 HTTP 接口可以测试我们插入的数据是否成功,但是如果直接将这个 API 暴露给 Web 应用程序将有极大的风险。这个 API 将会暴露管理功能(比如直接添加和删除文档),最理想的情况是完全不要对外暴露它。而是写一个简单的 Node.js API 去接收来自客户端的请求,然后(在我们的本地网络中)生成一个正确的查询发送给 Elasticsearch。
5.1 - 查询脚本
我们现在尝试从我们写的 Node.js 脚本中查询 Elasticsearch。
创建一个新文件,server/search.js
。
const { client, index, type } = require('./connection')
module.exports = {
/** Query ES index for the provided term */
queryTerm (term, offset = 0) {
const body = {
from: offset,
query: { match: {
text: {
query: term,
operator: 'and',
fuzziness: 'auto'
} } },
highlight: { fields: { text: {} } }
}
return client.search({ index, type, body })
}
}
我们的搜索模块定义一个简单的 search
函数,它将使用输入的词 match
查询。
这是查询的字段分解 -
from
- 允许我们分页查询结果。默认每个查询返回 10 个结果,因此,指定from: 10
将允许我们取回 10-20 的结果。query
- 这里我们指定要查询的词。operator
- 我们可以修改搜索行为;在本案例中,我们使用and
操作去对查询中包含所有字元(要查询的词)的结果来确定优先顺序。fuzziness
- 对拼写错误的容错调整,auto
的默认为fuzziness: 2
。模糊值越高,结果越需要更多校正。比如,fuzziness: 1
将允许以Patricc
为关键字的查询中返回与Patrick
匹配的结果。highlights
- 为结果返回一个额外的字段,这个字段包含 HTML,以显示精确的文本字集和查询中匹配的关键词。
你可以去浏览 Elastic Full-Text Query DSL,学习如何随意调整这些参数,以进一步自定义搜索查询。
6 - API
为了能够从前端应用程序中访问我们的搜索功能,我们来写一个快速的 HTTP API。
6.0 - API 服务器
用以下的内容替换现有的 server/app.js
文件。
const Koa = require('koa')
const Router = require('koa-router')
const joi = require('joi')
const validate = require('koa-joi-validate')
const search = require('./search')
const app = new Koa()
const router = new Router()
// Log each request to the console
app.use(async (ctx, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}`)
})
// Log percolated errors to the console
app.on('error', err => {
console.error('Server Error', err)
})
// Set permissive CORS header
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*')
return next()
})
// ADD ENDPOINTS HERE
const port = process.env.PORT || 3000
app
.use(router.routes())
.use(router.allowedMethods())
.listen(port, err => {
if (err) throw err
console.log(`App Listening on Port ${port}`)
})
这些代码将为 Koa.js Node API 服务器导入服务器依赖,设置简单的日志,以及错误处理。
6.1 - 使用查询连接端点
接下来,我们将在服务器上添加一个端点,以便于发布我们的 Elasticsearch 查询功能。
在 server/app.js
文件的 // ADD ENDPOINTS HERE
下面插入下列的代码。
/**
* GET /search
* Search for a term in the library
*/
router.get('/search', async (ctx, next) => {
const { term, offset } = ctx.request.query
ctx.body = await search.queryTerm(term, offset)
}
)
使用 docker-compose up -d --build
重启动应用程序。之后在你的浏览器中尝试调用这个搜索端点。比如,http://localhost:3000/search?term=java
这个请求将搜索整个图书馆中提到 “Java” 的内容。
结果与前面直接调用 Elasticsearch HTTP 界面的结果非常类似。
{
"took": 242,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 93,
"max_score": 13.356944,
"hits": [{
"_index": "library",
"_type": "novel",
"_id": "eHYHJmEBpQg9B4622421",
"_score": 13.356944,
"_source": {
"author": "Charles Darwin",
"title": "On the Origin of Species",
"location": 1080,
"text": "Java, plants of, 375."
},
"highlight": {
"text": ["<em>Java</em>, plants of, 375."]
}
}, {
"_index": "library",
"_type": "novel",
"_id": "2HUHJmEBpQg9B462xdNg",
"_score": 9.030668,
"_source": {
"author": "Unknown Author",
"title": "The King James Bible",
"location": 186,
"text": "10:4 And the sons of Javan; Elishah, and Tarshish, Kittim, and Dodanim."
},
"highlight": {
"text": ["10:4 And the sons of <em>Javan</em>; Elishah, and Tarshish, Kittim, and Dodanim."]
}
}
...
]
}
}
6.2 - 输入校验
这个端点现在还很脆弱 —— 我们没有对请求参数做任何的校验,因此,如果是无效的或者错误的值将使服务器出错。
我们将添加一些使用 Joi 和 Koa-Joi-Validate 库的中间件,以对输入做校验。
/**
* GET /search
* Search for a term in the library
* Query Params -
* term: string under 60 characters
* offset: positive integer
*/
router.get('/search',
validate({
query: {
term: joi.string().max(60).required(),
offset: joi.number().integer().min(0).default(0)
}
}),
async (ctx, next) => {
const { term, offset } = ctx.request.query
ctx.body = await search.queryTerm(term, offset)
}
)
现在,重启服务器,如果你使用一个没有搜索关键字的请求(http://localhost:3000/search
),你将返回一个带相关消息的 HTTP 400 错误,比如像 Invalid URL Query - child "term" fails because ["term" is required]
。
如果想从 Node 应用程序中查看实时日志,你可以运行 docker-compose logs -f api
。