封面来源:碧蓝航线 北境序曲 活动CG
本文参考:【狂神说Java】ElasticSearch7.6.x最新完整教程通俗易懂
本文内容基于 ElasticSearch 7.6.x,本文包含大量图片,请保持较好的网络连接。
1. ElasticSearch 概述
概述
Elasticsearch 是一个基于 Lucene 的 搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 语言开发的,并作为 Apache 许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch 用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据 DB-Engines 的排名显示,Elasticsearch 是最受欢迎的企业搜索引擎,其次是 Apache Solr,也是基于 Lucene。
Elasticsearch 是一个实时分布式搜索和分析引擎,它让你以前所未有的速度处理大数据成为可能。
主要用于全文搜索、结构化搜索、分析以及将这三者混合使用。
Elasticsearch 是一个基于 Apache Lucene™ 的开源搜索引擎。无论在开源还是专有领域,Lucene 可以 被认为是迄今为止先进、性能好的、功能全的搜索引擎库。
但是,Lucene 只是一个库。想要使用它,你必须使用 Java 来作为开发语言并将其直接集成到你的应用 中,更糟糕的是,Lucene 非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。 Elasticsearch 也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是 通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。
谁在使用
1、维基百科,实现全文搜索、高亮、搜索推荐等
2、The Guardian(国外新闻网站),用户行为日志、社交网络数据、数据分析、给到每篇新闻文章的作者,让他知道他的文章的公众反馈
3、Stack Overflow
4、GitHub
5、电商网站,检索商品
6、日志数据分析
7、商品价格监控网站
等等…
2. 与 Solr 的区别
2.1 Solr 的概述
Solr 是 Apache 下的一个顶级开源项目,采用 Java 语言开发,它是基于 Lucene 的全文搜索服务器。Solr 提供了比 Lucene 更为丰富的查询语言,同时实现了可配置、可扩展,并对索引、搜索性能进行了优化。
Solr 可以独立运行,运行在 Jetty、Tomcat 等这些 Servlet 容器中,Solr 索引的实现方法很简单,用 POST 方法向 Solr 服务器发送一个描述 Field 及其内容的 XML 文档,Solr 根据 XML 文档添加、删除、更新索引 。
Solr 搜索只需要发送 HTTP GET 请求,然后对 Solr 返回 XML、JSON 等格式的查询结果进行解析,组织页面布局。Solr 不提供构建 UI 的功能,Solr 提供了一个管理界面,通过管理界面可以查询 Solr 的配置和运行情况。
Solr 是基于 Lucene 开发企业级搜索服务器,实际上就是封装了 Lucene。Solr 是一个独立的企业级搜索应用服务器,它对外提供类似于 Web-service 的 API 接口。用户可以通过 Http 请求,向搜索引擎服务器提交一定格式的文件,生成索引,也可以通过提出查找请求,并得到返回结果。
2.2 Solr 与 ElasticSearch
参考链接:ElasticSearch和solr的差别
总结
1、ES 基本是开箱即用,非常简单,而 Solr 安装略微复杂一丢丢
2、Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能。
3、Solr 支持更多格式的数据,比如 JSON、XML、CSV,而 Elasticsearch 仅支持 JSON 文件格式。
4、Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供,例如图形化界面需要 Kibana 的支撑
5、Solr 查询快,但更新索引时慢(即插入删除慢),用于电商等查询多的应用;ES 建立索引快(即查询慢),即实时性查询快,用于 Facebook、新浪等搜索。Solr 是传统搜索应用的有力解决方案,但 Elasticsearch 更适用于新兴的实时搜索应用。
6、Solr比较成熟,有一个更大,更成熟的用户、开发和贡献者社区,而 Elasticsearch 相对开发维护者较少,更新太快,学习使用成本较高(这点已经不再是 ES 的缺点了)。
3. 安装 ElasticSearch
前置要求
Java 的版本至少是 JDK 8,ElasticSearch 的版本和我们以后要导入的依赖也要版本对应!
下载
官网链接:ElasticSearch
ELK 华为镜像:ElasticSearch、logstash、kibana
进入系统后,点击首页的下载按钮,进入下载页面,选择和自己系统相符的压缩包下载即可。
学习 ElasticSearch 可以使用 Linux,也可以使用 Windows,它对系统没有强制要求,为了减少不必要的操作,我们 选择在 Windows 环境上学习。
下载的 ELK 压缩包解压即可使用,因为涉及到图形化界面,所以需要一些必要的环境,比如:Node.js。
安装 ElasticSearch
在 Windows 下安装十分减压,解压即可。解压后如下图所示:
根据上面给出的示意图,来熟悉一些目录:
1 2 3 4 5 6 7 8
| bin 启动文件 config 配置文件 log4j2 日志配置文件 jvm.options Java 虚拟机相关的配置 elasticsearch.yml ES 的配置文件 默认端口号:9200 lib 相关 jar 包 modules 功能模块 plugins 插件,比如 ik 分词器
|
熟悉之后,在 Windows 上启动一下 ES,只需要前往 bin 目录下,双击 elasticsearch.bat 文件即可启动:
可以看到默认端口号确实是 9200。
不关闭服务,使用浏览器,在地址栏输入 localhost:9200
访问:
安装 ES 可视化界面
为了便于操作,我们可以安装一个 ES 的可视化界面,相当于一个插件。
这个插件名为:ElasticSearch-head
,前往 GitHub 搜索下载即可。这个插件很显然是一个前端工程,因此 Node.js 环境是必不可少的。
ElasticSearch-head 下载地址:ElasticSearch-head
下载完成后得到一个压缩包,解压到 ES 的同目录下,解压后的目录:
进入 head-master 目录下打开 CMD,执行命令 cnpm install
:
依赖下好后,执行命令 npm run start
启动可视化界面:
可以看到可视化界面的访问端口是 9100,在浏览器上试试:
那么问题也来了,一个端口是 9100,一个是 9200,这不就 跨域 了,那我们怎么解决呢?
解决也很简单,关闭 ES,进入 ES 的目录,找到 config 目录,找到 elasticsearch.yml
配置文件,进行配置即可。在文件内添加以下配置:
1 2
| http.cors.enabled: true http.cors.allow-origin: "*"
|
保存并退出,再次双击 elasticsearch.bat 文件运行 ES。这个时候查看可视化界面,点击连接,我们可以看到:
在上图中,可以看到有一个 索引 的选项。点击进入,然后新建一个名为 mofan 的索引:
在初学的时候,可以将 ES 当成一个数据库,然后可以建立库(索引),添加文档(库中的数据)。
将可视化界面 ElasticSearch-head 当成一个数据展示工具,书写查询语句在 Kibana 中操作。
4. 安装 Kibana
Kibana 简介
Kibana 是一个针对 Elasticsearch 的开源分析及可视化平台,用来搜索、查看交互存储在 Elasticsearch 索引中的数据。 使用 Kibana,可以通过各种图表进行高级数据分析及展示。Kibana 让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示 Elasticsearch 查询动态。
设置Kibana非常简单,无需编码或者额外的基础架构,几分钟内就可以完成 Kibana 安装并启动 Elasticsearch 索引监测。
安装 Kibana
官网下载地址:Kibana
下载 Kibana 时,需要注意其版本要和 ES 版本一致。下载完毕后,解压至 ES 相同的目录中,Kibana 是一个标准的前端化工程,别看它下载了只有将近 300M,解压还是需要一点时间的。
Kibana 解压完毕后的目录:
可以看到有一个 bin 目录,打卡它,能找到一个名为 kibana.bat 的文件,这就是启动文件,双击启动即可。
当然,在启动前需要先启动 ES,启动之后我们可以看到:
Kibana 默认的端口号是 5601。因此,可以在浏览器的地址栏输入本地 ip 与端口号,即可访问 Kibana,然后可以找到 开发者工具 的图标,点击即可进入。以后关于 ES 操作的语句我们都可以在开发者工具中书写:
我们还发现整个页面的语言都是英文的,Kibana 支持国际化,内置了中文语言,只不过需要设置一下。
进入 Kibana 的安装目录,找到 config 配置文件夹,然后找到 kibana.yml
文件进行配置。配置很简单,一句配置即可搞定:
修改了配置文件然后重启 Kibana,在浏览器地址栏输入:localhost:5601
再次查看:
奈斯!成功汉化!🎉
5. ES 的核心概念
安装完 ES 后,我们来熟悉一下 ES 中的一些核心概念,比如:集群、节点、索引、类型、文档、分片、映射等等。
ES 是面向文档的,一切都是 JSON,来看看关系型数据库与 ES 的一个小对比:
Relational DB |
ElasticSearch |
数据库(database) |
索引(indices) |
表(tables) |
types |
行(rows) |
documents |
字段(columns) |
fields |
ES 集群中可以包含多个索引(类似于数据库),每个索引中又可以包含多个类型(表),每个类型下又可以包含多个文档(行),每个文档下还包含多个字段(列)。
物理设计:
ES 在后台把每个索引划分成多个分片,每个分片可以在集群的不同服务器间迁移。
“一个人”就是一个集群,可以在 ElasticSearch-head
中看到默认集群名就叫:elasticsearch。
逻辑设计:
一个索引类型中,可以包含多个文档,比如:文档 1,文档 2。 当我们索引一篇文档时,可以通过这样一个顺序找到它:索引 ▷ 类型 ▷ 文档 ID,通过这个组合我们就能找到到某个具体的文档。
注意:ID 不必是整数,实际上它是个字符串。
5.1 文档
文档(document),就是一条条数据。比方说:
1 2 3
| user 1 MOFAN 18 2 yang 20
|
我们知道 ES 是面向文档的,那么就是说索引和数据搜索的最小单位是文档,ES 中,文档有几个重要属性:
-
自我包含,一篇文档同时包含字段和对应的值,也就是同时包含 key 和 value。
-
可以是层次型的,一个文档中包含自文档,复杂的逻辑实体就是这么来的
-
灵活的结构,文档不依赖预先定义的模式。我们知道关系型数据库中,要提前定义字段才能使用,在 ES 中,对于字段是非常灵活的,我们可以忽略该字段,或者动态的添加一个新的字段。
尽管我们可以随意的新增或者忽略某个字段,但是,每个字段的类型也非常重要,比如一个年龄字段类型,可以是字符串类型也可以是整形。因为 ES 会保存字段和类型之间的映射及其他的设置,这种映射具体到每个映射的每种类型,这也是为什么在 ES 中,类型有时候也称为映射类型。
5.2 类型
这里的类型和我们以前说的类型区别不大。类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。
类型中对于字段的定义称为映射,比如 name 映射为字符串类型。 文档是无模式的,它们不需要拥有映射中所定义的所有字段。比如新增一个字段,那么 ES 是怎么做的呢?
ES 会自动的将新字段加入映射,但是不知道这个字段是什么类型,因此 ES 就就会进行推测。比如这个值是18,那么 ES 会认为它是整型。 既然是预测,也就可能猜不对, 所以最安全的方式就是提前定义好所需要的映射,然后再使用,这就和关系型数据库差不多了。
5.3 索引
根据前面的对比,可以看出 ES 中的索引就相当于数据库。
索引是映射类型的容器,ES 中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置, 然后它们被存储到各个分片上。
使用 ElasticSearch-head
创建 ES 的索引时,就会出现分片数的设置:
一个集群中至少有一个节点,而一个节点就是一个 ES 进程,节点可以有多个索引默认。
按照上图来说,如果创建索引,那么索引将会有个 5 个分片(primary shard,又称主分片)构成,而每一个主分片还会有一个副本(replica shard,又称复制分片)。
简单来说,分片就是数据分成多个文件放在不同服务器上。
在上图是一个有 3 个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,如果某个节点挂了,数据也不会丢失。 实际上,一个分片是一个 Lucene 索引,一个包含 倒排索引 的文件目录,倒排索引的结构使得 ES 在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字。
那么什么是倒排索引?
5.4 倒排索引
简单来说,倒排索引就是分片之后,以关键词做 key,分片 ID 做 value。
ES 使用了一种称为倒排索引的结构,采用 Lucene 倒排索作为底层。这种结构适用于快速的全文搜索, 一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表。 例如,现在有两个文档, 每个文档包含如下内容:
1 2
| Study every day, good good up to forever # 文档1包含的内容 To forever, study every day, good good up # 文档2包含的内容
|
为了创建倒排索引,首先要将每个文档拆分成独立的词(或词条、tokens),然后创建一个包含所有不重复的词条的排序列表,然后列出每个词条出现在哪个文档,比如:
term |
doc_1 |
doc_2 |
Study |
√ |
× |
To |
× |
× |
every |
√ |
√ |
forever |
√ |
√ |
day |
√ |
√ |
study |
× |
√ |
good |
√ |
√ |
every |
√ |
√ |
to |
√ |
× |
up |
√ |
√ |
如果我们试图搜索 to forever
,就只需要查看包含每个词条的文档权重:
term |
doc_1 |
doc_2 |
to |
√ |
× |
forever |
√ |
√ |
total |
2 |
1 |
两个文档都匹配,但是第一个文档比第二个匹配度更高。
如果没有别的条件,两个包含关键词的文档都会返回。
再看一个示例,比如我们通过博客标签来搜索博客文章。倒排索引列表有这样的一个结构:
如果要搜索含有 python 标签的文章,那相对于查找所有原始数据而言,查找倒排索引后的数据将会快得多。只需要查看标签这一栏,然后获取相关的文章 ID 即可,这样可以完全过滤掉无关的所有数据。
ES 的索引和 Lucene 的索引对比
在 ES 中,索引被分为多个分片,每份分片是一个 Lucene 的索引。所以一个 ES 索引是由多个 Lucene 索引组成
的。
如无特指,说起索引都是指 ES 的索引。
接下来,我们在 Kibana 中的开发者工具中编写一下 ES 的操作语句。
6. IK 分词器
6.1 什么是 IK 分词器
所谓分词,就是把一段中文或者别的划分成一个个的关键字,我们在搜索时候会把自己的信息进行分词,并把数据库中或者索引库中的数据也进行分词,然后进行一个匹配操作,默认的中文分词是将每个字看成一个词。比如:“默烦帅逼”一段字,就会分割成“默”、“烦”、“帅”和“逼”四个字,这显然是不正确的。
如果要使用中文,建议使用 IK 分词器!
IK 提供了两个分词算法:ik_smart
和 ik_max_word
,其中 ik_smart
为最少切分,ik_max_word
为最细粒度划分!
6.2 下载安装
下载地址:Github 下载地址
下载完毕后(注意版本匹配),放入我们的 ES 目录中的 plugins 目录中:
然后重启 ES:
我们可以看到,解压之后运行 ES 就自动加载了 IK 分词器。
除此之外,可以在 ES 的 bin 目录下打开 CMD,然后运行 elasticsearch-plugin list
命令来查看加载的插件:
6.3 IK 分词器的使用
分词算法测试
安装完毕后,我们使用 Kibana 来测试一下:
先使用分词算法 ik_smart
进行测试,可以在左边看到结果,是一种最小切分(没有重复的字)。
再使用分词算法 ik_max_word
进行测试,也可以看到结果,将所有可能的组合都列了出来,是一种最细粒度的气切分(有重复的字)。
那为什么会切分成这样,很明显有一个字典在引导切分。
发现问题
再换个文本测试一下分词算法:
更换测试文本后,使用两种算法得到一样的结果。但这不是最重要,最重要的是:我们发现“默烦”两字被切分开了,显然是在字典中没有这个词,那怎么办呢?
就需要自己增加字典配置。
增加字典配置
操作很简单!
1、进入 IK 分词器的 config 目录,创建文件 mofan.dic
,文件内添加“默烦”两字,如:
2、然后打开这个目录下的 IKAnalyzer.cfg.xml
文件,拓展字典:
3、重启 ES,重启 Kibana。重启 ES 时,可以看到已经加载了我们配置的字典:
4、浏览器进入 Kibana 的开发者工具,输入与前文相同的一段字进行测试:
我们发现“默烦”两字没有被拆开,而是一个词,证明我们的字典配置正确!
7. 基本操作
7.1 Rest 风格说明
Rest 是一种软件架构风格,或者说是一种规范,其强调 HTTP 应当以资源为中心,并且规范了 URI 的风格,规范了 HTTP 请求动作(GET / PUT / POST / DELETE / HEAD / OPTIONS)的使用,具有对应的语义。
基本 Rest 命令说明:
method |
URL 地址 |
描述 |
PUT |
localhost:9200/索引名称/类型名称/文档id |
创建文档(指定文档id) |
POST |
localhost:9200/索引名称/类型名称 |
创建文档(随机文档id) |
POST |
localhost:9200/索引名称/类型名称/文档id/_update |
修改文档 |
DELETE |
localhost:9200/索引名称/类型名称/文档id |
删除文档 |
GET |
localhost:9200/索引名称/类型名称/文档id |
查询文档通过 id |
POST |
localhost:9200/索引名称/类型名称/_search |
查询所有数据 |
7.2 索引的基本操作
创建一个索引
先运行 elasticsearch-head
查看当前有哪些索引:
1 2 3
| .apm-agent-configuration .kibana_1 .kibana_task_manager_1
|
我们发现只有上述三个索引,并且都是 ES 自带的。
打开 Kibana,创建一个索引:
1 2
| PUT /索引/~类型名~/文档id {请求体}
|
执行命令后,前往 elasticsearch-head
看看是否真的成功创建了索引:
再看看数据是否成功添加:
指定字段类型
前面创建了一个索引,并向其中添加了数据,但是并没有指定数据的类型,那怎么指定数据的类型呢?
在 ES 中,有以下类型:
创建一个名为 test2 的索引,设置字段的类型,不添加数据:
获取信息
那我们怎么获取 test2 的信息呢?这里有一个 GET
的命令可以获取:
刚刚是获取了 test2 的信息,test2 中的字段是指定了类型的,那如果没指定类型呢?
创建一个新索引,直接插入数据,不指定数据类型:
再使用 GET
命令查看一下 test3 索引的信息:
很显然,如果文档的字段没有指定类型,ES 会默认配置字段类型。
GET 其他用法:
查看集群健康状态:
查看索引信息:
通过 GET _cat/
可以获得 ES 当前的很多信息,参考链接:curl命令操作elasticsearch
数据修改
可以直接通过 PUT
命令实现数据的修改,比如:
可以发现,_version
为 2,result
为 updated。
但这样有一个弊端,需要将没有修改的数据也写上,否则数据会丢失。
那还有其他的办法吗?
可以使用 POST
命令来更新数据:
删除索引
直接使用 DELETE 命令来实现删除,ES 会根据编写的命令来判断是删除文档还是删除索引,比如:
执行上述命令,删除索引 test1,去 elasticsearch-head
看看是否成功删除索引:
可以看到 test1 索引确实被删除了!
7.3 文档的基本操作
数据准备
1 2 3 4 5 6 7
| PUT /mofan/user/1 { "name": "张三", "age": 3, "desc": "法外狂徒", "tags":["渣男", "唱歌", "蹦迪"] }
|
按照这样的语法,插入三条数据:
获取数据(简单版)
更新数据
前文以及说了,可以使用 PUT
命令,也可以使用 POST
命令(推荐使用 POST
),比如:
1 2 3 4 5 6
| POST mofan/user/1/_update { "doc":{ "name": "王五" } }
|
简单的条件查询
查询 name 为李四的数据:
1
| GET mofan/user/_search?q=name:李四
|
7.4 复杂搜索
在前文的查询结果中,可以看到有一个名为 _score
的字段,这个字段就表示 权重 的意思。在有多个查询结果的时候,这个字段的值越大,就表示匹配度越高。
PS:百度就是利用这玩意赚钱的 🤮
前文涉及的查询是一种简单查询,通过参数的方式进行查询,但是实际使用时我们一般不会这么做。
现在对 mofan 索引增加一条数据:
1 2 3 4 5 6 7
| PUT mofan/user/4 { "name": "默烦", "age": 18, "desc": "小菜鸡", "tags": ["学习", "写博客", "看番"] }
|
假设要查询 name 为“默烦”的数据,像前文一样,可以这么查询:
1
| GET mofan/user/_search?q=name:默
|
这相当于一种省略的写法,其实完整的写法是这样的:
1 2 3 4 5 6 7 8
| GET mofan/user/_search { "query": { "match": { "name": "默" } } }
|
这样都可以查询出对应的数据:
假设又添加一条数据:
1 2 3 4 5 6 7
| PUT mofan/user/5 { "name": "默小烦", "age": 6, "desc": "菜鸡", "tags": ["学习", "写博客", "看番"] }
|
然后再次查询:
可以看到,查询得到了两条结果(图片大小所限,只能显示一条,但从 hits 的 value 可见,确实得到了两条结果),两条结果的排序按照 _score
大小进行降序排序,同时需要注意 hits 的作用,在 Java API 中也有一个类型的类。
查询结果字段过滤
上面的查询会将所有字段都查询出来,如果想 查询指定的字段 该怎么做呢?
类似于 MySQL 中的 select *
变成了 select name, desc
:
查询结果排序
前面的结果的排序是按照 _score
降序排序的,那如果要指定排序规则呢?比如按照 age 进行升序排序。
相当于 MySQL 中的 order by
:
分页
数据太多,想进行分页查询该怎么办呢?
相当于 MySQL 中的 limit
:
在【查询结果排序】中,一共查得两条数据,第一条 name 为“默小烦”,第二条 name 为“默烦”。我们这里从第二条数据开始(第一条数据的 form 值为 0),每页一条数据,因此可以查得 name 为“默烦”的数据。
注意: 数据小标从 0 开始,第一条数据的索引是 0。
布尔值查询
如果想要多条件查询,应该怎么办呢?可以使用布尔值查询。
比如查询 name 中带有“默”的,且 age 为 6 的数据。使用 must
命令,相当于 MySQL 中的 and
:
既然有 and
那么就有 or
,在 ES 中可以使用 should
命令:
图片大小所限,只能显示一条结果,但从 value 的值来看,确实查询出 两条 数据。
那如果要查询 name 中不带有“默”字的数据应该怎么操作呢?
可以使用 must_not
操作,相当于 MySQL 中的 not
:
图片大小所限,只能显示一条结果,但从 value 的值来看,确实查询出 三条 数据。因为 name 中不带有“默”字的有三条数据,分别是:李四、王五和鲲鲲。
那如果要进行范围查询呢?比如查询 name 中带有“默”字的,且 age 大于 16 的数据。在 ES 中可以使用 filter
:
当然也可以进行多条件过滤:
多条件匹配
如果我想要根据 tags 进行查询,查询 tags 中包含“博客”和“男”的数据,应该怎么做?
针对同一字段的多个条件,使用空格隔开就可以了,而且只要满足其中一个条件都会被查询出来,可以通过 _score
进行判断。
查询结果有三条,分别是 name 为“默烦”、“默小烦”和“王五”的数据(上图中省略了第一条 name 为“默烦”的数据)。
精确查询
前文涉及的查询都是一种 模糊查询,但如果要进行精确查询应该怎么办?
term 查询是直接通过 倒排索引 指定的词条进程精确查找的。
关于分词:
term:直接查询精确
match:会使用分词器解析(先分析文档,然后在通过分析的文档进行查询)
很显然,使用 term 效率更高!
两个类型的解析:test 和 keyword
又创建一个索引 testdb,并添加两条数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| PUT testdb { "mappings": { "properties": { "name":{ "type": "text" }, "desc":{ "type": "keyword" } } } }
PUT testdb/_doc/1 { "name": "默烦 name", "desc": "默烦 desc" }
PUT testdb/_doc/2 { "name": "默烦 name", "desc": "默烦 desc2" }
|
我们先来测试一下:
换种分词算法:
查询 name 中带有“默”的数据:
name 是 text 类型的,可以看到能查询出两条结果,再来试试查询 desc 中带有“默”的数据:
由于 desc 的类型是 keyword
的,这个类型的值不会被分词器解析。也就是说,查询 desc 中带有“默”的数据只会查询出 desc 的值为“默”的数据,很显然并没有,因此不会查询出数据。换种方式试试:
这样的话就可以精确匹配了! 🎉
多个值匹配精确查询
先往 testdb 索引中插入两条数据:
1 2 3 4 5 6 7 8 9 10 11
| PUT testdb/_doc/3 { "t1": "22", "t2": "2020-11-15" }
PUT testdb/_doc/4 { "t1": "33", "t2": "2020-11-16" }
|
实现精确查询,需要使用 term
;实现多个值匹配,需要使用 should
。
因此,要实现多个值匹配查询,只需要将两者结合就可以了:
高亮查询
除此之外,ES 还支持一个很 牛逼 的功能——高亮查询!
高亮查询就像百度、淘宝、京东搜索某些东西时,会在搜索结果中根据搜索的内容进行高亮显示。比如,我想将 mofan 索引中,name 字段值带有“默烦”的文档高亮显示出来:
那如果不想让高亮字段被 <em>
标签包裹起来呢?ES 支持自定义高亮包裹标签:
当然,这些功能 MySQL 也能做到,但是效率就不如 ES 了。
8. 集成 SpringBoot
8.1 集成准备
看官方文档,官方怎么搞,我们怎么搞。
官方文档链接:ES docs
进入文档后,可以看到:
点击 Elasticsearch Clients
进入:
由于官网版本是 7.10,而我们的版本是 7.6,因此点击 other versions
,选择 7.6 的版本:
进入后,可以看到:
点击 Java High Level REST Client
旁边的 + 号,然后点击 Getting started
:
点击 Maven Repository
找到原生依赖:
1 2 3 4 5
| <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.6.2</version> </dependency>
|
点击右边的 Initialization
:
接下来,使用 IDEA 导入对应的依赖,分析一下这个类中的方法。
配置基本的项目
SpringBoot 需要导入的依赖:
项目创建完毕后,查看一下导入的依赖:
注意: 一定要保证导入的依赖和 ES 版本一致!
因此,需要在 pom.xml 文件中进行配置:
1 2 3 4 5
| <properties> <java.version>1.8</java.version> <elasticsearch.version>7.6.1</elasticsearch.version> </properties>
|
我在此顺便也将 SpringBoot 的版本降至 2.2.6。
编写一个配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package indi.mofan.config;
@Configuration public class ElasticSearchConfig { @Bean public RestHighLevelClient restHighLevelClient() { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("127.0.0.1", 9200, "http")) ); return client; } }
|
在项目的依赖中,找到 ES 自动配置的依赖:
查看源码中提供的对象:
从上图中可以看出,虽然 @Import
导入了三个类,其实都是内部类,核心类只有一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class RestClientConfigurations {
@Configuration(proxyBeanMethods = false) static class RestClientBuilderConfiguration { }
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(RestHighLevelClient.class) static class RestHighLevelClientConfiguration { }
@Configuration(proxyBeanMethods = false) static class RestClientFallbackConfiguration { } }
|
8.2 索引 API 测试
创建索引
在 SpringBoot 项目的测试类中进行编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @SpringBootTest class EsApiApplicationTests {
@Autowired @Qualifier("restHighLevelClient") private RestHighLevelClient client;
@Test void testCreateIndex() throws IOException { CreateIndexRequest request = new CreateIndexRequest("mofan_index"); CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT); System.out.println(createIndexResponse); } }
|
运行上述代码,启动 ElasticSearch-head
,然后访问 9100 端口,可以看到 mofan_index 索引已经创建成功:
判断索引是否存在
1 2 3 4 5 6 7
| @Test void testExistIndex() throws IOException { GetIndexRequest request = new GetIndexRequest("mofan_index"); boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); System.out.println(exists); }
|
删除索引
1 2 3 4 5 6 7 8
| @Test void testDeleteIndex() throws IOException { DeleteIndexRequest request = new DeleteIndexRequest("mofan_index"); AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT); System.out.println(response.isAcknowledged()); }
|
运行上述代码,启动 ElasticSearch-head
,然后访问 9100 端口,可以看到 mofan_index 索引已经删除成功:
为了便于后续测试查看,我们删除除 mofan_index
以外的所有索引:
8.3 文档 API 测试
为便于使用 JSON,在 pom.xml 中导入 fastjson
依赖:
1 2 3 4 5 6 7
|
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency>
|
创建一个实体类:
1 2 3 4 5 6 7 8 9 10
|
@Data @NoArgsConstructor @AllArgsConstructor public class User { private String name; private int age; }
|
创建文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test void testAddDocument() throws IOException { User user = new User("默烦", 18); IndexRequest request = new IndexRequest("mofan_index"); request.id("1").timeout(TimeValue.timeValueSeconds(1)); IndexRequest source = request.source(JSON.toJSONString(user), XContentType.JSON); IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT); System.out.println(indexResponse.toString()); System.out.println(indexResponse.status()); }
|
运行上述代码后,进 9100 端口查看:
测试某一文档是否存在
1 2 3 4 5 6 7 8 9 10 11
| @Test void testIsExistDocument() throws IOException { GetRequest getRequest = new GetRequest("mofan_index", "1"); getRequest.fetchSourceContext(new FetchSourceContext(false)); getRequest.storedFields("_none_");
boolean exists = client.exists(getRequest, RequestOptions.DEFAULT); System.out.println(exists); }
|
获取文档的内容
1 2 3 4 5 6 7 8 9 10
| @Test void testGetDocument() throws IOException { GetRequest getRequest = new GetRequest("mofan_index", "1"); GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(response.getSourceAsString()); System.out.println(response); }
|
运行后,控制台输出:
1 2
| {"age":18,"name":"默烦"} {"_index":"mofan_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":18,"name":"默烦"}}
|
更新文档的信息
1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testUpdateDocument() throws IOException { UpdateRequest updateRequest = new UpdateRequest("mofan_index", "1"); updateRequest.timeout("1s");
User user = new User("mofan", 5); updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = client.update(updateRequest, RequestOptions.DEFAULT); System.out.println(updateResponse.status()); }
|
运行上述代码后,前往 9100 端口查看:
删除文档记录
1 2 3 4 5 6 7 8 9
| @Test void testDeleteRequest() throws IOException { DeleteRequest deleteRequest = new DeleteRequest("mofan_index", "1"); deleteRequest.timeout("1s");
DeleteResponse deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT); System.out.println(deleteResponse.status()); }
|
运行上述代码后,前往 9100 端口查看:
批量插入文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Test void testBulkRequest() throws IOException { BulkRequest bulkRequest = new BulkRequest(); bulkRequest.timeout("1s");
ArrayList<User> userList = new ArrayList<>(); userList.add(new User("mofan1", 5)); userList.add(new User("mofan2", 6)); userList.add(new User("mofan3", 7)); userList.add(new User("mofan4", 8)); userList.add(new User("mofan5", 9)); userList.add(new User("mofan6", 10));
for (int i = 0; i < userList.size(); i++) { bulkRequest.add( new IndexRequest("mofan_index") .id("" + (i + 1)) .source(JSON.toJSONString(userList.get(i)), XContentType.JSON) ); }
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT); System.out.println(bulkResponse.hasFailures()); }
|
运行上述代码后,前往 9100 端口查看:
查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Test void testSearch() throws IOException { SearchRequest searchRequest = new SearchRequest("mofan_index"); SearchSourceBuilder builder = new SearchSourceBuilder(); TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "mofan1"); builder.query(termQueryBuilder); builder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchRequest.source(builder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); System.out.println(JSON.toJSONString(searchResponse.getHits())); System.out.println("==============================="); for (SearchHit documentFields : searchResponse.getHits().getHits()) { System.out.println(documentFields.getSourceAsMap()); } }
|
上述代码实现了精确查询,控制台输出结果:
1 2 3
| {"fragment":true,"hits":[{"fields":{},"fragment":false,"highlightFields":{},"id":"1","matchedQueries":[],"primaryTerm":0,"rawSortValues":[],"score":1.540445,"seqNo":-2,"sortValues":[],"sourceAsMap":{"name":"mofan1","age":5},"sourceAsString":"{\"age\":5,\"name\":\"mofan1\"}","sourceRef":{"fragment":true},"type":"_doc","version":-1}],"maxScore":1.540445,"totalHits":{"relation":"EQUAL_TO","value":1}} =============================== {name=mofan1, age=5}
|
此处的 name 字段值如果是中文,由于使用了默认的分词器,将会导致查询失败!
其他查询 API:
termQuery(key, obj)
:单个精准匹配
termsQuery(key, obj1, obj2..)
:多个精准匹配
matchQuery(key, Obj)
:单个匹配
multiMatchQuery(text, field1, field2...)
:多字段匹配
matchAllQuery()
:匹配所有文件
9. 京东搜索小 Demo
9.1 初始化 Demo
关闭前面创建的 Module,在当前工程下新建一个 Module。创建一个 SpringBoot 项目,导入以下依赖:
创建好项目后,更改 pom.xml 文件,修改 SpringBoot 版本至 2.2.6,设置 ES 的版本,导入 FastJSON 的依赖:
1 2 3 4 5 6 7 8 9 10 11 12
| <properties> <java.version>1.8</java.version> <elasticsearch.version>7.6.1</elasticsearch.version> </properties>
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency>
|
然后导入必要的素材,比如 JS、CSS和图片。
修改 application.properties 配置文件:
1 2 3
| server.port=9090
spring.thymeleaf.cache=false
|
接下来,就是数据的问题了。
9.2 爬取数据
我们可以使用 JSoup 来解析网页,因此需要导入对应的依赖:
1 2 3 4 5 6 7
|
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.10.2</version> </dependency>
|
需要将解析的网页内容放入一个对象中,因此需要自定义一个对象:
1 2 3 4 5 6 7 8 9 10 11
|
@Data @NoArgsConstructor @AllArgsConstructor public class Content { private String title; private String img; private String price; }
|
然后就解析网页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
@Component public class HtmlParseUtil {
public List<Content> parseJD(String keywords) throws IOException{
String url = "https://search.jd.com/Search?keyword=" + keywords;
Document document = Jsoup.parse(new URL(url), 30000); Element element = document.getElementById("J_goodsList");
Elements elements = element.getElementsByTag("li");
ArrayList<Content> goodsList = new ArrayList<>(); for (Element el : elements) { String img = el.getElementsByTag("img").eq(0).attr("data-lazy-img"); String price = el.getElementsByClass("p-price").eq(0).text(); String title = el.getElementsByClass("p-name").eq(0).text();
Content content = new Content(); content.setTitle(title); content.setPrice(price); content.setImg(img); goodsList.add(content); } return goodsList; } }
|
9.3 业务编写
同样,需要 ES 的配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@Configuration public class ElasticSearchConfig { @Bean public RestHighLevelClient restHighLevelClient() { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("127.0.0.1", 9200, "http")) ); return client; } }
|
编写 Service,为了简洁,就不面向接口了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| package indi.mofan.service;
import com.alibaba.fastjson.JSON; import indi.mofan.pojo.Content; import indi.mofan.utils.HtmlParseUtil; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.indices.GetIndexRequest; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service;
import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit;
@Service public class ContentService {
@Autowired @Qualifier("restHighLevelClient") private RestHighLevelClient client;
@Autowired private RestHighLevelClient restHighLevelClient;
public Boolean parseContent(String keywords) throws IOException { List<Content> contents = new HtmlParseUtil().parseJD(keywords);
GetIndexRequest getIndexRequest = new GetIndexRequest("jd_goods"); if (!client.indices().exists(getIndexRequest, RequestOptions.DEFAULT)) { CreateIndexRequest request = new CreateIndexRequest("jd_goods"); client.indices().create(request, RequestOptions.DEFAULT); }
BulkRequest bulkRequest = new BulkRequest(); bulkRequest.timeout("2m");
for (int i = 0; i < contents.size(); i++) { bulkRequest.add( new IndexRequest("jd_goods") .source(JSON.toJSONString(contents.get(i)), XContentType.JSON)); }
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT); return !bulk.hasFailures(); }
public List<Map<String, Object>> searchPage(String keyword, int pageNo, int pageSize) throws IOException { if (pageNo <= 1) { pageNo = 1; }
SearchRequest searchRequest = new SearchRequest("jd_goods"); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.from(pageNo); sourceBuilder.size(pageSize);
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", keyword); sourceBuilder.query(termQueryBuilder); sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchRequest.source(sourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
ArrayList<Map<String, Object>> list = new ArrayList<>(); for (SearchHit documentFields : searchResponse.getHits().getHits()) { list.add(documentFields.getSourceAsMap()); } return list; } }
|
编写控制器,同时还可以进行简单的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@RestController public class ContentController {
@Autowired private ContentService contentService;
@GetMapping("/parse/{keyword}") public Boolean parse(@PathVariable("keyword") String keyword) throws IOException { return contentService.parseContent(keyword); }
@GetMapping("/search/{keyword}/{pageNo}/{pageSize}") public List<Map<String, Object>> search(@PathVariable("keyword") String keyword, @PathVariable("pageNo") int pageNo, @PathVariable("pageSize") int pageSize) throws IOException { return contentService.searchPage(keyword, pageNo, pageSize); } }
|
可以打开浏览器,输入对应的地址,向 ES 中插入一些数据,也可以进行简单的搜索。
比如:localhost:9090/parse/java
就可以向 ES 中添加关于 Java 书籍的信息。由于京东商品页做了分页,因此添加数据是一次 30 条数据。
又比如:http://localhost:9090/search/java/1/10
就可以从 ES 中查出 10 条关于 Java 书籍的信息。
9.4 前后端交互
要实现前后端交互,需要使用到 Vue,因此也就需要下载 Vue 的一些 JS 文件。我们可以在桌面(当然,任何地方都行)创建一个文件夹(最好命名不含中文),然后使用 Git 或 CMD,在这个文件夹内下载需要的依赖。
1 2 3
| npm init cnpm install vue cnpm install axios
|
当然,前提需要在所用电脑上安装 Node.js 和 cnpm,可以参看【Node.js 的安装与 Hexo 的升级】一文。
然后将 \node_modules\axios\dist\
目录下的 axios.min.js
复制到我们的项目中,同样需要将 \node_modules\vue\dist
目录下的 vue.min.js
文件复制到我们的项目中。
最后在前端页面引用这两个 JS 文件。如:
1 2 3 4
|
<script th:src="@{/js/vue.min.js}"></script> <script th:src="@{/js/axios.min.js}"></script>
|
其实,更推荐使用 CDN 的方式应用 JS,不然后续可能会出现找不到对应 JS 文件的情况:
1 2 3
| <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.min.js"></script> <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
|
然后就在前端页面书写对应的 JS 代码,整个前端页面代码在本文末给出。
编写完毕之后,重启 IDEA,再运行项目。
注意: 记得保持 ES 处于运行状态!
实现高亮
在 Serveice 层添加一个和高亮相关的 searchPageHighlightBuilder
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| public List<Map<String, Object>> searchPageHighlightBuilder(String keyword, int pageNo, int pageSize) throws IOException { if (pageNo <= 1) { pageNo = 1; }
SearchRequest searchRequest = new SearchRequest("jd_goods"); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.from(pageNo); sourceBuilder.size(pageSize);
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", keyword); sourceBuilder.query(termQueryBuilder); sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.requireFieldMatch(false); highlightBuilder.field("title").preTags("<span style='color:red'>").postTags("</span>"); sourceBuilder.highlighter(highlightBuilder);
searchRequest.source(sourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
ArrayList<Map<String, Object>> list = new ArrayList<>(); for (SearchHit hit : searchResponse.getHits().getHits()) {
Map<String, HighlightField> highlightFields = hit.getHighlightFields(); HighlightField title = highlightFields.get("title"); Map<String, Object> sourceAsMap = hit.getSourceAsMap(); if (title != null) { Text[] fragments = title.fragments(); String n_title = ""; for (Text text : fragments) { n_title += text; } sourceAsMap.put("title", n_title); } list.add(sourceAsMap); } return list; }
|
修改 Controller 层的 search
方法,在这个方法中调用高亮显示方法:
1 2 3 4 5 6
| @GetMapping("/search/{keyword}/{pageNo}/{pageSize}") public List<Map<String, Object>> search(@PathVariable("keyword") String keyword, @PathVariable("pageNo") int pageNo, @PathVariable("pageSize") int pageSize) throws IOException { return contentService.searchPageHighlightBuilder(keyword, pageNo, pageSize); }
|
9.5 前端页面资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org">
<head> <meta charset="utf-8"/> <title>商品搜索-京东</title> <link rel="stylesheet" th:href="@{/css/style.css}"/> </head>
<body class="pg"> <div class="page" id="app"> <div id="mallPage" class=" mallist tmall- page-not-market ">
<div id="header" class=" header-list-app"> <div class="headerLayout"> <div class="headerCon "> <h1 id="mallLogo"> <img th:src="@{/images/jdlogo.png}" alt=""> </h1>
<div class="header-extra">
<div id="mallSearch" class="mall-search"> <form name="searchTop" class="mallSearch-form clearfix"> <fieldset> <legend>搜索</legend> <div class="mallSearch-input clearfix"> <div class="s-combobox" id="s-combobox-685"> <div class="s-combobox-input-wrap"> <input v-model="keyword" type="text" autocomplete="off" value="dd" id="mq" class="s-combobox-input" aria-haspopup="true"> </div> </div> <button type="submit" @click.prevent="searchKey" id="searchbtn">搜索</button> </div> </fieldset> </form> <ul class="relKeyTop"> <li><a>spring</a></li> <li><a>python</a></li> <li><a>java核心技术</a></li> <li><a>mysql</a></li> <li><a>java编程思想</a></li> </ul> </div> </div> </div> </div> </div>
<div id="content"> <div class="main"> <form class="navAttrsForm"> <div class="attrs j_NavAttrs" style="display:block"> <div class="brandAttr j_nav_brand"> <div class="j_Brand attr"> <div class="attrKey"> 品牌 </div> <div class="attrValues"> <ul class="av-collapse row-2"> <li><a href="#"> 狂神说 </a></li> <li><a href="#"> Java </a></li> </ul> </div> </div> </div> </div> </form>
<div class="filter clearfix"> <a class="fSort fSort-cur">综合<i class="f-ico-arrow-d"></i></a> <a class="fSort">人气<i class="f-ico-arrow-d"></i></a> <a class="fSort">新品<i class="f-ico-arrow-d"></i></a> <a class="fSort">销量<i class="f-ico-arrow-d"></i></a> <a class="fSort">价格<i class="f-ico-triangle-mt"></i><i class="f-ico-triangle-mb"></i></a> </div>
<div class="view grid-nosku">
<div class="product" v-for="result in results"> <div class="product-iWrap"> <div class="productImg-wrap"> <a class="productImg"> <img :src="result.img"> </a> </div> <p class="productPrice"> <em>{{result.price}}</em> </p> <p class="productTitle"> <a v-html="result.title"> </a> </p> <div class="productShop"> <span>店铺: </span> </div> <p class="productStatus"> <span>月成交<em>999笔</em></span> <span>评价 <a>3</a></span> </p> </div> </div> </div> </div> </div> </div> </div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.min.js"></script> <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script> <script>
new Vue({ el: '#app', data: { keyword: '', results: [] }, methods: { searchKey() { let keyword = this.keyword; console.log(keyword); axios.get('search/' + keyword + "/1/10").then(response=>{ console.log(response.data); this.results = response.data; }) } } })
</script>
</body> </html>
|
ElasticSearch 入门完