Lucene
Lucene 基于 Java 的全文信息检索工具包,不是一个完整的搜索应用程序,而是为你的应用程序提供索引和搜索功能。
目前有很多应用程序的搜索功能基于 Lucene,比如 Eclipse 的帮助系统的搜索功能。Lucene 能够为文本类型的数据建立索引,只要能把你要索引的数据格式转化为文本,Lucene 就能对你的文档进行索引和搜索。 比如一些 HTML 文档,PDF 文档进行索引,需要把 HTML 文档和 PDF 转化为文本格式。然后把转化后的内容交给 Lucene 索引。最后把创建好的索引文件保存到磁盘或者内存中。根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式,使得 Lucene 能够几乎适用于所有的搜索应用程序。
索引是现代搜索引擎的核心,建立索引的过程就是把元数据处理成方便查询的索引文件的过程。
Lucene 软件包的发布形式是 jar 包。主要的 java 包有
org.apache.lucene.document,提供了一些为封装要索引的文档所需要的类,比如 Document, Feild 这样,每一个文档最终被封装成了一个 Document 对象。
org.apache.lucene.analysis,对文档进行分词,文档在建立索引之前必须进行分词,这个包的作用可以看成为建立索引做准备工作。
org.apache.lucene.index,这个包提供了一些类来协助创建索引以及对创建好的索引进行更新。这里有两个基础的类:IndexWriter 和 IndexReader,其中 IndexWriter 是用来创建索引,并且添加文档到索引中,IndexReader 用来删除索引中的文档。
org.apache.lucene.search,这个包对建立好的索引上进行搜索,IndexSearcher 和 Hits,IndexSearcher 定义了指定的索引上进行搜索的方法,Hits 保存搜索得到的结果。
建立索引 在电脑的目录中,含有很多个文本文档,我们需要查找哪些文档中有某个关键词。
为了实现这种功能,先利用 Lucene 对这个目录中的文档建立索引,然后再建立好的索引中搜索我们需要查找的文档。
为了对文档进行索引,Lucene 提供了5个基础类,Document,Field,IndexWriter,Analyzer,Directory。
Document
描述文档,这里的文档可以指一个 HTML 页面,一封电子邮件,一个文本文件。一个 Document 由多个 Field 对象组成,把一个 Document 对象想象成数据库中的一个记录,每个 Field 对象就是记录的一个字段。
Field
描述文档的某个属性,比如一封电子邮件的标题和内容可以用两个 Field 对象分别描述。
Analyzer
在一个文档被索引之前,首先需要对文档内容进行分词处理,这部分工作就是由 Analyzer 来做的,Analyzer 类是一个抽象类,有多个实现,针对不同的语言和应用,选择合适的 Analyzer。把分词后的内容交给 IndexWriter 来建立索引。
IndexWriter
Lucene 创建索引的核心类,把一个个的 Document 对象添加到索引中
Directory
Lucene 的索引的存储位置,抽象类,FSDirectory,表示存储在文件系统中的索引位置,RAMDirectory 表示一个存储在内存中的索引的位置。
熟悉建立索引需要的这些类后,就开始对某个目录下面的文本文件建立索引。
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 package TestLucene; import java.io.File; import java.io.FileReader; import java.io.Reader; import java.util.Date; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexWriter; public class TxtFileIndexer { public static void main (String[] args) throws Exception { File indexDir = new File("D:\\luceneIndex" ); File dataDir = new File("D:\\luceneData" ); Analyzer luceneAnalyzer = new StandardAnalyzer(); File[] dataFiles = dataDir.listFiles(); IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true ); long startTime = new Date().getTime(); for (int i = 0 ; i < dataFiles.length; i++){ if (dataFiles[i].isFile() && dataFiles[i].getName().endsWith(".txt" )){ System.out.println("Indexing file " + dataFiles[i].getCanonicalPath()); Document document = new Document(); Reader txtReader = new FileReader(dataFiles[i]); document.add(Field.Text("path" ,dataFiles[i].getCanonicalPath())); document.add(Field.Text("contents" ,txtReader)); indexWriter.addDocument(document); } } indexWriter.optimize(); indexWriter.close(); long endTime = new Date().getTime(); System.out.println("It takes " + (endTime - startTime) + " milliseconds to create index for the files in directory " + dataDir.getPath()); } }
IndexWriter 构造函数需要三个参数,第一个参数指定了创建的索引要存放的位置,可以是 File 对象,也可以是 FSDirectory 或者 RAMDirectory 对象。第二个参数指定了 Analyzer 类的一个实现,指定这个索引是用哪个分词器对文档内容进行分词。第三个参数是一个布尔型的变量,如果为 true 的话就代表创建一个新的索引,为 false 的话就代表在原来索引的基础上进行操作。程序遍历了目录下的所有文本文档,并且为每个文本文档创建了一个 Document 对象。
把文本文档的两个属性:路径和内容加入到两个 Field 对象中,把这个两个对象加入到 Document 对象中,把文档用 IndexWriter 类的 add 方法加入到索引中去,这样就完成了索引的创建。
搜索文档 在上一个部分中,已经为一个目录下的文本文档建立好了索引,现在就在索引的基础上搜索包含某个关键字或者短语的文档。Lucene 提供了几个基础的类完成这个过程 IndexSearcher,Term,Query,TermQuery,Hits
Query
这是一个抽象类,他有多个实现,比如 TermQuery,BooleanQuery, PrefixQuery,这个类的目的是吧用户输入的查询字符串封装成 Lucene 能够识别的 Query。
Term
是搜索的基本单位,一个 Term 对象有两个 String 类型的域组成。生成一个 Term 对象可以 Term term = new Term(“fieldName”, “queryWord”);第一个参数代表要在文档的哪一个 Field 上进行查找,第二个参数代表要查询的关键词。
TermQuery
TermQuery 是抽象类 Query 的一个子类,同时也是 Lucene 支持的最基本的查询类,生成 TermQeury 对象:TermQuery termQuery = new TermQuery(new Term("fieldName", "queryWord"));
构造函数只接受一个 Term 对象。
IndexSearcher
用来在建立好的索引上进行搜索,只能以只读的方式打开一个索引,可以多个 IndexSearcher 的实例在一个索引上进行操作。
Hits
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 package TestLucene; import java.io.File; import org.apache.lucene.document.Document; import org.apache.lucene.index.Term; import org.apache.lucene.search.Hits; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TermQuery; import org.apache.lucene.store.FSDirectory; public class TxtFileSearcher { public static void main (String[] args) throws Exception { String queryStr = "lucene" ; File indexDir = new File("D:\\luceneIndex" ); FSDirectory directory = FSDirectory.getDirectory(indexDir,false ); IndexSearcher searcher = new IndexSearcher(directory); if (!indexDir.exists()){ System.out.println("The Lucene index is not exist" ); return ; } Term term = new Term("contents" ,queryStr.toLowerCase()); TermQuery luceneQuery = new TermQuery(term); Hits hits = searcher.search(luceneQuery); for (int i = 0 ; i < hits.length(); i++){ Document document = hits.doc(i); System.out.println("File: " + document.get("path" )); } } }
IndexSearcher 的构造函数接受一个类型为 Directory 的对象,传入两个子类:FSDirectory 和 RAMDirectory 传入一个 FSDirectory 对象作为其参数,代表一个存储的磁盘上的索引的位置。
构造完 IndexSearcher 后,代表以只读的方式打开一个索引,程序构造了一个 Term 对象,通过这个 Term 对象,指定了要在文档的内容中搜索包含关键词 lucene 的文档,利用这个 Term 创建出 TermQuery 对象并且把这个 TermQuery 对象传入到 IndexSearcher 的 search 方法中查询。返回结果保存在 Hits 对象中。
Elasticsearch
基于 Lucene 的搜索服务器,分布式多用户的全文搜索引擎,基于 RESTful web 接口。实时搜索、稳定、快速、安装使用方便。
建立一个网站或者应用程序,并要添加搜索功能,想要完成搜索工作的创建是非常困难的。希望能够简单的使用 Json 通过 Http 来索引数据,希望搜索服务器始终可用,从一台开始扩展到数百台,实时搜索,建立一个云的解决方案,利用 Elasticsearch 解决这些问题及可能出现的更多问题。
6.x 以后,索引内置能有一个类型,后续计划删除类型这个概念,ES6 开始让索引名和类型名一致。
索引 ES将数据存储于一个或者多个索引中,索引是具有类似特性的文档的集合,一个 ES 集群可以按需创建任意数量的索引
映射 Mapping,索引库中的字段名及其数据类型进行定义,类似于 mysql 中的表结构信息。 es 的mapping 比数据库灵活的多,尅动态识别字段,一般不需要指定 mapping,es 会自动根据数据格式识别它的类型,如果需要对某些字段添加特殊属性(定义使用其他分词器、是否分词、是否存储),就必须手动添加 mapping
文档 文档是 lucene 索引和搜索的原子单位,包含了一个或者多个域的容器,基于 json 格式表示,每个域拥有一个名字以及一个或多个值。
ES 的分片(shard) 机制,可以将一个索引内部的数据分布存储于多个节点,通过将一个索引切分为多个底层物理的 lucene 索引完成索引数据的分割存储功能,每一个物理的 lucene 索引成为一个分片(shard),每个分片其内部都是一个全功能且独立的索引,所以可由集群中的任何主机存储,创建索引时,用户可指定分片的数量,默认为5。
shard 有两种类型 primary 和 replica,主副,Replica shard 是 primary shard 的副本。也可以配置多个。 ES 会根据需要自动增加或者减少 Replica shard 的数量。
安装 Elasticsearch
1 2 3 4 brew install elasticsearch brew install kibana 启动 elasticsearch kibana
分词器
分词器,把文本内容按照标准进行切分,内容转为小写,去掉标点,遇到每个中文字符,都当成一个单词处理。如果要对中文进行分词,安装中文分词器插件 ik
默认分词器
1 2 3 4 5 6 PUT /shop_product GET /shop_product/_analyze { "text": "I am Groot" }
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 { "tokens" : [ { "token" : "i" , "start_offset" : 0 , "end_offset" : 1 , "type" : "<ALPHANUM>" , "position" : 0 }, { "token" : "am" , "start_offset" : 2 , "end_offset" : 4 , "type" : "<ALPHANUM>" , "position" : 1 }, { "token" : "groot" , "start_offset" : 5 , "end_offset" : 10 , "type" : "<ALPHANUM>" , "position" : 2 } ] }
安装 ik 分词器
Elasticsearch 版本 6.8.3 Analysis-ik 6.8.3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 mingle的个人空间 elastic 正文 Mac OS 安装 Elasticsearch 使用 analysis-ik mingle mingle 发布于 2018/10/08 17:42 字数 199 阅读 604 收藏 0 点赞 0 评论 0 Mac OS XElasticSearch 系统: Mac OS X Elasticsearch 版本: 5.6.4 Analysis-ik 5.6.4 # 更新 brew $ brew update # 修改formula $ cd /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula $ git checkout 5874ac8c8fab69f8a714d643581e489bcb176d92 elasticsearch@5.6.rb # 安装 $ brew install elasticsearch@5.6 # 查看 $ brew info elasticsearch@5.6 # 锁定版本, 防止被误升级 $ brew pin elasticsearch@5.6 # 安装ik $ cd /usr/local/Cellar/elasticsearch@5.6/5.6.4 $ bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.8.3/elasticsearch-analysis-ik-6.8.3.zip
使用
1 2 3 4 5 6 7 # 查看状态 $ brew services list # 启动/重启/停止 $ brew services start elasticsearch $ brew services start kibana $ brew services restart elasticsearch $ brew services stop elasticsearch
分词效果如下: ik_smart 粗粒度分词,ik_max_word 细粒度分词
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 GET /shop_product/_analyze { "text" : "英特尔酷睿i7处理器" , "analyzer" : "ik_smart" } { "tokens" : [ { "token" : "英特尔" , "start_offset" : 0 , "end_offset" : 3 , "type" : "CN_WORD" , "position" : 0 }, { "token" : "酷" , "start_offset" : 3 , "end_offset" : 4 , "type" : "CN_CHAR" , "position" : 1 }, { "token" : "睿" , "start_offset" : 4 , "end_offset" : 5 , "type" : "CN_CHAR" , "position" : 2 }, { "token" : "i7" , "start_offset" : 5 , "end_offset" : 7 , "type" : "LETTER" , "position" : 3 }, { "token" : "处理器" , "start_offset" : 7 , "end_offset" : 10 , "type" : "CN_WORD" , "position" : 4 } ] }
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 GET /shop_product/_analyze { "text" : "英特尔酷睿i7处理器" , "analyzer" : "ik_max_word" } { "tokens" : [ { "token" : "英特尔" , "start_offset" : 0 , "end_offset" : 3 , "type" : "CN_WORD" , "position" : 0 }, { "token" : "英特" , "start_offset" : 0 , "end_offset" : 2 , "type" : "CN_WORD" , "position" : 1 }, { "token" : "尔" , "start_offset" : 2 , "end_offset" : 3 , "type" : "CN_CHAR" , "position" : 2 }, { "token" : "酷" , "start_offset" : 3 , "end_offset" : 4 , "type" : "CN_CHAR" , "position" : 3 }, { "token" : "睿" , "start_offset" : 4 , "end_offset" : 5 , "type" : "CN_CHAR" , "position" : 4 }, { "token" : "i7" , "start_offset" : 5 , "end_offset" : 7 , "type" : "LETTER" , "position" : 5 }, { "token" : "i" , "start_offset" : 5 , "end_offset" : 6 , "type" : "ENGLISH" , "position" : 6 }, { "token" : "7" , "start_offset" : 6 , "end_offset" : 7 , "type" : "ARABIC" , "position" : 7 }, { "token" : "处理器" , "start_offset" : 7 , "end_offset" : 10 , "type" : "CN_WORD" , "position" : 8 }, { "token" : "处理" , "start_offset" : 7 , "end_offset" : 9 , "type" : "CN_WORD" , "position" : 9 }, { "token" : "处" , "start_offset" : 7 , "end_offset" : 8 , "type" : "COUNT" , "position" : 10 }, { "token" : "理" , "start_offset" : 8 , "end_offset" : 9 , "type" : "CN_CHAR" , "position" : 11 }, { "token" : "器" , "start_offset" : 9 , "end_offset" : 10 , "type" : "CN_CHAR" , "position" : 12 } ] }
拓展词库 找到 IK 插件中的 config/main.dic 文件,往里面添加新词汇,重启服务器即可。
倒排索引
将文档id 和 文档数据,以分词为索引重新排列,在查找时,根据分词进行查找,找到文档的 id ,然后找出文档内容展示。
以往都是根据文档 id 查询文档内容,而分词的方式,是根据分词查询文档 id,所以叫倒排索引。
基本操作
建立索引,相当于建立数据库
1 2 3 4 5 6 7 8 PUT /索引名 在没有特殊设置的情况下,默认有5个分片,1个备份,也可以通过请求参数的方式来指定 { "settings" : { "number_of_shards": 5, //设置5个片区 "number_of_replicas": 1 //设置1个备份 } }
删除索引 DELETE /索引名
映射操作,建立映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 语法:PUT 索引 { "mappings" : { 类型名: { "properties": { 字段名: { "type": 字段类型, "analyzer": 分词器类型, "search_analyzer": 分词器类型, ... }, ... } } } } 字段类型:double / long / integer / text / keyword / date / binary 注意:text和keyword都是字符串类型,但是只有text类型的数据才能分词,字段的配置一旦确定就不能更改 映射的配置项有很多,我们可以根据需要只配置用得上的属性
查询映射 GET/索引名/_mapping
CRUD 操作
少量的增删改,主要做查询。查询性能很高。启动项目的时候添加数据。添加的数据从其他数据库中导入。不会主动生产数据,数据来源于其他数据库。
增/改 语法
1 2 3 4 5 6 PUT /索引名/类型名/文档ID { field1: value1, field2: value2, ... }
注意 当索引/类型/映射不存在时,会使用默认设置自动添加 ES中的数据一般是从别的数据库导入的,所以文档的ID 会沿用原数据库中的 ID 索引库中没有该 ID 对应的文档时,则新增,有的时候就替换。
文档内置字段
_index 索引
_type 类型
_id 文档ID
_version 乐观锁版本号
_source 数据内容
查询
语法:根据 ID 查询 -> GET /索引名/类型/文档ID 查询所有 -> GET /索引名/类型/_search
文档内置字段
took 耗时
_shards.total 分片总数
hits.total 查询到的数量
hits.max_score 最大匹配度 可能大于1.0,查询所有是 1.0
hits.hits 查询到的结果
hits.hits._score 匹配度
删除
语法:DELETE /索引/类型/文档ID
这里的删除不是真正意义上的删除,仅仅是清空文档内容而已,并且标记该文档的状态为删除
高级查询
Elasticsearch 基于 JSON 提供完整的查询 DSL (Domain Specific Language 领域特定语言) 来定义查询
领域特定语言
基本语法 GET /索引/类型/_search 一般都是需要配合查询参数来使用的,配合不同的参数有不同的查询效果。加入如下数据,来体验高级查询
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 POST /shop_product/shop_product/_bulk {"create" :{"_id" : 1 }} {"id" :1 ,"title" :"Apple iPhone XR (A2108) 128GB 白色 移动联通电信4G手机 双卡双待" ,"price" :5299 ,"intro" :"【iPhoneXR限时特惠!】6.1英寸视网膜显示屏,A12仿生芯片,面容识别,无线充电,支持双卡!选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!" ,"brand" :"Apple" } {"create" :{"_id" : 2 }} {"id" :2 ,"title" :"Apple 2019款 Macbook Pro 13.3【带触控栏】八代i7 18G 256G RP645显卡 深空灰 苹果笔记本电脑 轻薄本 MUHN2CH/A" ,"price" :15299 ,"intro" :"【八月精选】Pro2019年新品上市送三重好礼,现在购买领满8000减400元优惠神劵,劵后更优惠!" ,"brand" :"Apple" } {"create" :{"_id" : 3 }} {"id" :3 ,"title" :"Apple iPad Air 3 2019年新款平板电脑 10.5英寸(64G WLAN版/A12芯片/Retina显示屏/MUUL2CH/A)金色" ,"price" :3788 ,"intro" :"8月尊享好礼!买iPad即送蓝牙耳机!领券立减!多款产品支持手写笔!【新一代iPad,总有一款适合你】选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!" ,"brand" :"Apple" } {"create" :{"_id" : 4 }} {"id" :4 ,"title" :"华为HUAWEI MateBook X Pro 2019款 英特尔酷睿i5 13.9英寸全面屏轻薄笔记本电脑(i5 8G 512G 3K 触控) 灰" ,"price" :7999 ,"intro" :"3K全面屏开启无界视野;轻薄设计灵动有型,HuaweiShare一碰传" ,"brand" :"华为" } {"create" :{"_id" : 5 }} {"id" :5 ,"title" :"华为 HUAWEI Mate20 X (5G) 7nm工艺5G旗舰芯片全面屏超大广角徕卡三摄8GB+256GB翡冷翠5G双模全网通手机" ,"price" :6199 ,"intro" :"【5G双模,支持SA/NSA网络,7.2英寸全景巨屏,石墨烯液冷散热】5G先驱,极速体验。" ,"brand" :"华为" } {"create" :{"_id" : 6 }} {"id" :6 ,"title" :"华为平板 M6 10.8英寸麒麟980影音娱乐平板电脑4GB+64GB WiFi(香槟金)" ,"price" :2299 ,"intro" :"【华为暑期购】8月2日-4日,M5青春版指定爆款型号优惠100元,AI语音控制" ,"brand" :"华为" } {"create" :{"_id" : 7 }} {"id" :7 ,"title" :"荣耀20 PRO DXOMARK全球第二高分 4800万四摄 双光学防抖 麒麟980 全网通4G 8GB+128GB 蓝水翡翠 拍照手机" ,"price" :3199 ,"intro" :"白条6期免息!麒麟980,4800万全焦段AI四摄!荣耀20系列2699起,4800万超广角AI四摄!" ,"brand" :"荣耀" } {"create" :{"_id" : 8 }} {"id" :8 ,"title" :"荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD 指纹解锁)冰河银" ,"price" :6199 ,"intro" :"16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,指纹一健开机登录,魔法一碰传高速传输。" ,"brand" :"荣耀" } {"create" :{"_id" : 9 }} {"id" :9 ,"title" :"荣耀平板5 麒麟8核芯片 GT游戏加速 4G+128G 10.1英寸全高清屏影音平板电脑 WiFi版 冰川蓝" ,"price" :1549 ,"intro" :"【爆款平板推荐】哈曼卡顿专业调音,10.1英寸全高清大屏,双喇叭立体环绕音,配置多重护眼,值得拥有!" ,"brand" :"荣耀" } {"create" :{"_id" : 10 }} {"id" :10 ,"title" :"小米9 4800万超广角三摄 6GB+128GB全息幻彩蓝 骁龙855 全网通4G 双卡双待 水滴全面屏拍照智能游戏手机" ,"price" :2799 ,"intro" :"限时优惠200,成交价2799!索尼4800万广角微距三摄,屏下指纹解锁!" ,"brand" :"小米" } {"create" :{"_id" : 11 }} {"id" :11 ,"title" :"小米(MI)Pro 2019款 15.6英寸金属轻薄笔记本(第八代英特尔酷睿i7-8550U 16G 512GSSD MX250 2G独显) 深空灰" ,"price" :6899 ,"intro" :"【PCIE固态硬盘、72%NTSC高色域全高清屏】B面康宁玻璃覆盖、16G双通道大内存、第八代酷睿I7处理器、专业级调校MX150" ,"brand" :"小米" } {"create" :{"_id" : 12 }} {"id" :12 ,"title" :"联想(Lenovo)拯救者Y7000P 2019英特尔酷睿i7 15.6英寸游戏笔记本电脑(i7 9750H 16G 1T SSD GTX1660Ti 144Hz)" ,"price" :9299 ,"intro" :"超大1T固态,升级双通道16G内存一步到位,GTX1660Ti电竞级独显,英特尔9代i7H高性能处理器,144Hz电竞屏窄边框!" ,"brand" :"联想" }
结果排序 1 2 3 4 5 6 7 参数格式: { "sort" : [ {field: 排序规则}, ... ] }
排序规则:asc 表示升序,desc 表示降序。 没有配置排序的情况下,默认按照评分降序排列。
分页查询 1 2 3 4 5 参数格式: { "from" : start, "size" : pageSize }
需求1:查询所有文档按照价格降序排列
1 2 3 4 5 6 7 8 9 10 11 # 需求1:查询所有文档按照价格降序排列 GET /shop_product/shop_product/_search { "sort": [ { "price": { "order": "desc" } } ] }
需求2:分页查询文档按照价格降序排列,显示第2页,每页显示3个
1 2 3 4 5 6 7 8 9 10 11 12 GET /shop_product/shop_product/_search { "sort": [ { "price": { "order": "desc" } } ], "from": 1, "size": 3 }
检索查询 1 2 3 4 5 6 参数格式: { "query" : { 检索方式: {field: value} } }
检索方式:
term 表示精确匹配,value 值不会被分词器拆分,按照倒排索引匹配
match 表示全文检索,value 值会被分词器拆分,然后去倒排索引中匹配
range 表示范围检索,其 value 值是一个对象,如{ “range”: {field: {比较规则: value, …}} },比较规则有 gt/gte/lt/lte 等
注意 term 和 match 都能用在数值和字符上,range 多用在数值上
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 # 需求1:查询商品标题中符合"游戏 手机"的字样的商品 GET /shop_product/shop_product/_search { "query" : { "match" : { "title" : "游戏 手机" } } } # 需求2:查询商品价格等于15299的商品 GET /shop_product/shop_product/_search { "query" : { "term" : { "price" : 15299 } } } # 需求3:查询商品价格在5000~10000之间商品,按照价格升序排列 GET /shop_product/shop_product/_search { "query" : { "range" : { "price" : { "gt" : 5000 , "lt" : 10000 } } } }
关键字查询 1 2 3 4 5 6 7 8 9 参数格式: { "query" : { "multi_match" : { "query" : value, "fields" : [field1, field2, ...] } } }
multi_match 表示在多个字段间做检索,只要其中一个字段满足条件就能查询出来,多用在字符上。
1 2 3 4 5 6 7 8 9 10 # 需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品 GET /shop_product/shop_product/_search { "query" : { "multi_match" : { "query" : "蓝牙 指纹 双卡" , "fields" : ["title" ,"intro" ] } } }
高亮显示 1 2 3 4 5 6 7 8 9 10 11 12 13 参数格式: { "query" : {...}, "highlight" : { "fields" : { field1 : {}, field2 : {}, ... } }, "pre_tags": 开始标签, "post_tags": 结束标签 }
highlight: 表示高亮显示,需要在 fields 中配置哪些字段中检索到该内容需要高亮显示,必须配合检索(term/match) 一起使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品,并且高亮显示 GET /shop_product/shop_product/_search { "query" : { "multi_match" : { "query" : "蓝牙 指纹" , "fields" : ["title" ,"intro" ] } }, "highlight" : { "fields" : { "title" : {}, "intro" : {} }, "pre_tags" : "<span style='color:red'>" , "post_tags" : "</span>" } }
逻辑查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 参数格式: { "query" : { "bool" : { 逻辑规则: [ { 检索方式: { field: value }, ... } ], ... } } }
逻辑规则:must/should/must_not, 相当于 and/or/not
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 # 需求1:查询商品标题中符合"i7"的字样并且价格大于7000的商品 GET /shop_product/shop_product/_search { "query" : { "bool" : { "must" : [ {"range" : { "price" : { "gte" : 7000 } }}, { "match" : { "title" : "i7" } } ] } } } # 需求2:查询商品标题中符合"pro"的字样或者价格在1000~3000的商品 GET /shop_product/shop_product/_search { "query" : { "bool" : { "should" : [ {"range" : { "price" : { "gt" : 1000 , "lt" : 3000 } }}, { "match" :{ "title" : "pro" } } ] } } }
过滤查询 1 2 3 4 5 6 7 8 9 10 11 参数格式: { "query" : { "bool" : { "filter" : [ {检索方式: {field: value}}, ... ] } } }
从效果上,过滤查询和检索查询一样,区别是过滤查询不评分,结果可以缓存。检索查询要评分,结果不缓存,一般不会直接使用过滤查询。都是在检索了一定数据的基础上采用。
query 和 filter 的区别
分组查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 参数格式: { "size" : 0 , "aggs" : { 自定义分组字段名: { "terms": { // 按照什么来分组 "field": 分组字段, "order": {自定义统计字段: 排序规则}, "size": 10 // 默认显示10组 }, "aggs": { // 分组后的统计查询,相当于 mysql 分组查询函数 自定义统计字段: { 分组运算: { "field": 统计字段 } } } } } }
分组运算 avg/sum/min/max/value_count/stats(执行以上所有功能),size=0目的是为了不显示 hit 内容,专注点放在分组上。
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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 # 需求1:按照品牌分组,统计各品牌的数量 GET /shop_product/shop_product/_search { "size" : 0 , "aggs" : { "groupByBrand" : { "terms" : { "field" : "brand.keyword" } } } } { "took" : 6 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 12 , "max_score" : 0.0 , "hits" : [ ] }, "aggregations" : { "groupByBrand" : { "doc_count_error_upper_bound" : 0 , "sum_other_doc_count" : 0 , "buckets" : [ { "key" : "Apple" , "doc_count" : 3 }, { "key" : "华为" , "doc_count" : 3 }, { "key" : "荣耀" , "doc_count" : 3 }, { "key" : "小米" , "doc_count" : 2 }, { "key" : "联想" , "doc_count" : 1 } ] } } } # 需求2:按照品牌分组,统计各品牌的平均价格 GET /shop_product/shop_product/_search { "size" : 0 , "aggs" : { "groupByBrand" : { "terms" : { "field" : "brand.keyword" , "order" : {"avgPrice" : "desc" } }, "aggs" : { "avgPrice" : { "avg" : { "field" : "price" } } } } } } { "took" : 24 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 12 , "max_score" : 0.0 , "hits" : [ ] }, "aggregations" : { "groupByBrand" : { "doc_count_error_upper_bound" : 0 , "sum_other_doc_count" : 0 , "buckets" : [ { "key" : "联想" , "doc_count" : 1 , "avgPrice" : { "value" : 9299.0 } }, { "key" : "Apple" , "doc_count" : 3 , "avgPrice" : { "value" : 8128.666666666667 } }, { "key" : "华为" , "doc_count" : 3 , "avgPrice" : { "value" : 5499.0 } }, { "key" : "小米" , "doc_count" : 2 , "avgPrice" : { "value" : 4849.0 } }, { "key" : "荣耀" , "doc_count" : 3 , "avgPrice" : { "value" : 3649.0 } } ] } } } # 需求3:按照品牌分组,统计各品牌的价格数据 GET /shop_product/shop_product/_search { "size" : 0 , "aggs" : { "groupByBrand" : { "terms" : { "field" : "brand.keyword" }, "aggs" : { "avgPrice" : { "stats" : { "field" : "price" } } } } } }
批处理 需要集中的批量处理文档,如果使用单个 API,会浪费大量资源,ES 为提高性能,专门提供了批处理 API
mget 批量查询 1 2 3 4 5 6 7 8 语法: GET /索引名/类型/_mget { "docs" : [ {"_id" : 文档ID}, ... ] }
bulk 批量增删改 1 2 3 4 5 6 7 8 9 10 语法 POST /索引名/类型/_bulk { 动作: {"_id": 文档ID} } {...} { 动作: {"_id": 文档ID} } {...}
动作:create/update/delete,其中 delete 只有1行 json,其他都是2行 json,并且 json 不能格式化,如果是 update 动作,它的数据需要加个 key:doc。{"update": {"_id": xx}}
{"doc": {"xx": xx, "xx": xx}}
Spring Data Elasticsearch
准备工作 pom.xml
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 <?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns ="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.1.3.RELEASE</version > <relativePath /> </parent > <groupId > cn.lizhaoloveit</groupId > <artifactId > elasticsearch</artifactId > <version > 1.0</version > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-elasticsearch</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > commons-beanutils</groupId > <artifactId > commons-beanutils</artifactId > <version > 1.8.3</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
application.properties
1 2 3 4 spring.data.elasticsearch.cluster-name =elasticsearch spring.data.elasticsearch.cluster-nodes =localhost:9300
编写 domain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @NoArgsConstructor @Setter @Getter @Document (indexName = "shop_product" , type = "shop_product" )public class product { @Id private Long id; @Field (type = FieldType.Keyword, analyzer = "ik_smart" , searchAnalyzer = "ik_smart" ) private String title; @Field (type = FieldType.Long) private Integer price; @Field (type = FieldType.Keyword, analyzer = "ik_smart" , searchAnalyzer = "ik_smart" ) private String intro; }
组件介绍
ElasticsearchTemplate:框架封装的用于便捷操作Elasticsearch的模板类
ElasticsearchRepository:框架封装的用于便捷完成常用操作的工具接口
NativeSearchQueryBuilder:用于生成查询条件的构建器,需要去封装各种查询条件
QueryBuilder:该接口表示一个查询条件,其对象可以通过 QueryBuilders 工具类中的方法快速生成各种条件
boolQuery() 生成 bool 条件,相当于 "bool": {}
matchQuery() 生成 match 条件,相当于 "match": {}
rangeQuery() 生成 range 条件,相当于 "range": {}
AbstractAggregationBuilder:用于生成分组查询的构建器,其对象通过 AggregationBuilders 工具类生成
PageAble: 表示分页参数,对象通过 PageRequest.of(页数, 容量)获取
SortBuilder 排序构建器,对象通过 SortBuilders.fieldSort(字段).order(规则)获取
ElasticsearchTemplate 模板类,封装了便捷操作 Elasticsearch 的模板方法,包裹索引/映射/CRUD 等底层操作和高级操作,该对象用起来复杂写,尤其对于查询,需要把查询到的结果自己封装对象。
1 2 3 @Autowired private ElasticsearchTemplate template;
ElasticsearchRepository 接口是框架封装用于操作 Elasticearch 的高级接口,我们自己写个接口去继承该接口就能直接对 Elasticsearch 进行 CRUD 操作。
1 2 3 4 5 6 7 8 9 @Repository public interface ProductESRepository extends ElasticsearchRepository <User , Long > { }
一般情况下, ElasticsearchTemplate 和 ElasticsearchRepository 是分工合作的,ElasticsearchRepository 已经能完成绝大部分的功能,遇到复杂的查询使用 ElasticsearchTmplate,如多字段分组、高亮显示等等。
实例代码如下 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 @Autowired private ProductESRepository repository;@Test public void saveOrUpdate () { Product product = new Product(); product.setId(13l ); product.setTitle("11" ); product.setPrice(19l ); product.setIntro("22" ); repository.save(product); } @Test public void delete () { repository.deleteById(13l ); } @Test public void delete () { Optional<Product> optional = repository.findById(13l ); } @Test public void delete () { Iterable<Product> all = repository.findAll(); }
高级查询
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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 @Autowired private ProductESRepository repository;@Test public void testQuery1 () { Iterable<Product> all = repository.findAll(Sort.by(Sort.Direction.DESC, "price" )); all.forEach(System.out::println); } @Test public void testQuery2 () { Sort sort = Sort.by(Sort.Direction.DESC, "price" ); PageRequest of = PageRequest.of(1 , 3 , sort); Iterable<Product> all = repository.findAll(of); all.forEach(System.out::println); } @Test public void testQuery3 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withPageable(PageRequest.of(1 , 3 )); builder.withSort(SortBuilders.fieldSort("price" ).order(SortOrder.DESC)); Page<Product> page = repository.search(builder.build()); System.out.println(page.getTotalElements()); System.out.println(page.getTotalPages()); page.forEach(System.out::println); } @Test public void testQuery4 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery(QueryBuilders.matchQuery("title" , "游戏 手机" )); Page<Product> page = repository.search(builder.build()); page.forEach(System.out::println); } @Test public void testQuery5 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery(QueryBuilders.termQuery("price" , 15299 )); Page<Product> page = repository.search(builder.build()); page.forEach(System.out::println); } @Test public void testQuery6 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.rangeQuery("price" ).gte(5000 ).lte(9000 ) ); builder.withSort(SortBuilders.fieldSort("price" ).order(SortOrder.ASC)); Page<Product> page = repository.search(builder.build()); page.forEach(System.out::println); } @Test public void testQuery7 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡" , "title" , "intro" ) ); Page<Product> page = repository.search(builder.build()); page.forEach(System.out::println); } @Test public void testQuery8 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("title" , "i7" )) .must(QueryBuilders.rangeQuery("price" ).gt(7000 )) ); Page<Product> page = repository.search(builder.build()); page.forEach(System.out::println); } @Test public void testQuery9 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.boolQuery() .should(QueryBuilders.matchQuery("title" , "pro" )) .should(QueryBuilders.rangeQuery("price" ).gte(1000 ).lte(3000 )) ); Page<Product> page = repository.search(builder.build()); page.forEach(System.out::println); } @Test public void testQuery10 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.addAggregation( AggregationBuilders.terms("groupByBrand" ).field("brand.keyword" ) ); AggregatedPage<Product> page = (AggregatedPage<Product>) repository.search(builder.build()); StringTerms groupByBrand = (StringTerms) page.getAggregation("groupByBrand" ); List<StringTerms.Bucket> buckets = groupByBrand.getBuckets(); for (StringTerms.Bucket bucket : buckets) { System.out.println(bucket.getKey()); System.out.println(bucket.getDocCount()); } } @Test public void testQuery11 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.addAggregation( AggregationBuilders.terms("groupByBrand" ).field("brand.keyword" ) .subAggregation(AggregationBuilders.avg("avgPrice" ).field("price" )) ); AggregatedPage<Product> page = (AggregatedPage<Product>) repository.search(builder.build()); StringTerms groupByBrand = (StringTerms) page.getAggregation("groupByBrand" ); List<StringTerms.Bucket> buckets = groupByBrand.getBuckets(); for (StringTerms.Bucket bucket : buckets) { System.out.println(bucket.getKey()); System.out.println(bucket.getDocCount()); InternalAvg avgPrice = bucket.getAggregations().get("avgPrice" ); System.out.println(avgPrice.getValue()); } } @Test public void testQuery12 () { NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.addAggregation( AggregationBuilders.terms("groupByBrand" ).field("brand.keyword" ) .subAggregation(AggregationBuilders.stats("statsPrice" ).field("price" )) ); AggregatedPage<Product> page = (AggregatedPage<Product>) repository.search(builder.build()); StringTerms groupByBrand = (StringTerms) page.getAggregation("groupByBrand" ); List<StringTerms.Bucket> buckets = groupByBrand.getBuckets(); for (StringTerms.Bucket bucket : buckets) { System.out.println(bucket.getKey()); System.out.println(bucket.getDocCount()); InternalStats statsPrice = bucket.getAggregations().get("statsPrice" ); System.out.println(statsPrice.getAvg()); System.out.println(statsPrice.getMax()); System.out.println(statsPrice.getMin()); System.out.println(statsPrice.getSum()); } }
高亮显示 需求,查询商品标题或者简介中符合 “蓝牙 指纹 双卡” 的字样的商品,并且高亮显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void testQueryHighlight () { String preTag = "<span style='color:red'>" ; String postTag = "</span>" ; NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡" , "title" , "intro" ) ); builder.withHighlightFields( new HighlightBuilder.Field("title" ) .preTags(preTag).postTags(postTag), new HighlightBuilder.Field("intro" ) .preTags(preTag).postTags(postTag) ); AggregatedPage<Product> products = template.queryForPage(builder.build(), Product.class); products.forEach(System.out::println); }
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 # 需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品,并且高亮显示 GET /shop_product/shop_product/_search { "query" : { "multi_match" : { "query" : "蓝牙 指纹" , "fields" : ["title" ,"intro" ] } }, "highlight" : { "fields" : { "title" : {}, "intro" : {} }, "pre_tags" : "<span style='color:red'>" , "post_tags" : "</span>" } } "hits" : { "total" : 6, "max_score" : 2.8370674, "hits" : [ { "_index" : "shop_product" , "_type" : "shop_product" , "_id" : "8" , "_score" : 2.8370674 , "_source" : { "id" : 8 , "title" : "荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD 指纹解锁)冰河银" , "price" : 6199 , "intro" : "16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,指纹一健开机登录,魔法一碰传高速传输。" , "brand" : "荣耀" }, "highlight" : { "intro" : [ "16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,<span style='color:red'>指</span><span style='color:red'>纹</span>一健开机登录,魔法一碰传高速传输。" ], "title" : [ "荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD <span style='color:red'>指</span><span style='color:red'>纹</span>解锁)冰河银" ] } }, ... ] }
上述代码并不能满足需求,原因是在执行 template.queryForPage
的,在对结果封装的时候,默认的封装模式,会在结果中直接找到 hits 中的 hits,然后从 hits 中继续找到 _source
中的内容根据传入的 class 进行结果集的封装。 所以如果得到的 intro 或者 title 中直接含有高亮的字段的代码,需要自己解析封装结果。template.queryForPage
方法的第三个参数,匿名构造器,中写入对结果集的封装。
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 @Test public void testQueryHighlight () { ObjectMapper mapper = new ObjectMapper(); String preTag = "<span style='color:red'>" ; String postTag = "</span>" ; NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡" , "title" , "intro" ) ); builder.withHighlightFields( new HighlightBuilder.Field("title" ) .preTags(preTag).postTags(postTag), new HighlightBuilder.Field("intro" ) .preTags(preTag).postTags(postTag) ); AggregatedPage<Product> products = template.queryForPage(builder.build(), Product.class, new SearchResultMapper() { @Override public <T> AggregatedPage<T> mapResults (SearchResponse searchResponse, Class<T> aClass, Pageable pageable) { List<T> content = new ArrayList<>(); for (SearchHit hit : searchResponse.getHits().getHits()) { try { T t = mapper.readValue(hit.getSourceAsString(), aClass); content.add(t); } catch (IOException e) { e.printStackTrace(); return null ; } } return new AggregatedPageImpl<>( content, pageable, searchResponse.getHits().getTotalHits() ); } }); products.forEach(System.out::println); }
上面是没有高亮的结果。也是默认的结果集的封装代码。 需要在添加到 content 之前,将 highlight 中的数据替换到 bean 中对应的属性上。
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 @Test public void testQueryHighlight () { ObjectMapper mapper = new ObjectMapper(); String preTag = "<span style='color:red'>" ; String postTag = "</span>" ; NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡" , "title" , "intro" ) ); builder.withHighlightFields( new HighlightBuilder.Field("title" ) .preTags(preTag).postTags(postTag), new HighlightBuilder.Field("intro" ) .preTags(preTag).postTags(postTag) ); AggregatedPage<Product> products = template.queryForPage(builder.build(), Product.class, new SearchResultMapper() { @Override public <T> AggregatedPage<T> mapResults (SearchResponse searchResponse, Class<T> aClass, Pageable pageable) { List<T> content = new ArrayList<>(); for (SearchHit hit : searchResponse.getHits().getHits()) { try { T t = mapper.readValue(hit.getSourceAsString(), aClass); for (HighlightField field : hit.getHighlightFields().values()) { String name = field.getName(); Object value = field.getFragments()[0 ]; BeanUtils.setProperty(t, name, value); } content.add(t); } catch (IOException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } return new AggregatedPageImpl<>( content, pageable, searchResponse.getHits().getTotalHits() ); } }); products.forEach(System.out::println); }