搜索数据的常用方式
1、数据库模糊查询
使用like.
2、使用数据库全文索引
mysql 使用ngram插件实现了全文索引功能,可以在指定字段中进行搜索。
3、solrlucenees
https://blog.csdn.net/u014209975/article/details/53263642
lucene是什么:
全文检索:
将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对快的目的。这部分从
非结构化数据中提取出的,然后重新组织的信息,我们称之为索引。
这种先建立索引,再对索引进行搜索的过程就叫做全文检索。
倒排索引
将数据加入到索引库(你可以理解成另外一个数据库)时,会先提取数据中的词汇(分词),将词汇加入到文档域,文档域中记录了词汇以及词汇在哪条数据记录中出现过的数据下标。用户在搜索数据时,先将用户搜索的数据进行词汇提取,然后把对应词汇拿到索引域中进行匹配查找,查找后会找到对应的下标ID,再根据对应下标ID到文档域中找真实数据.
非结构化数据搜索方式
1、顺序扫描:
速度过慢,占用资源。
2、全文索引
索引流程及原理图:
2.1 获得文档。
包括本地文档,网络文档等
2.2 创建文档对象
2.3分析文档
创建索引代码实现
Field的一些选择
搜索索引代码实现
https://blog.csdn.net/weixin_42633131/article/details/82873731
倒排索引
得到正向索引的结构如下:
“文档1”的ID > 单词1:出现次数,出现位置列表;单词2:出现次数,出现位置列表;…………。
“文档2”的ID > 此文档出现的关键词列表。
所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
得到倒排索引的结构如下:
“关键词1”:“文档1”的ID,“文档2”的ID,…………。
“关键词2”:带有此关键词的文档ID列表。
个人认为翻译成转置索引可能比较合适。
一个未经处理的数据库中,一般是以文档ID作为索引,以文档内容作为记录。
而Inverted index 指的是将单词或记录作为索引,将文档ID作为记录,这样便可以方便地通过单词或记录查找到其所在的文档。
Lucene、Solr、Elasticsearch关系
Lucene:底层的API,工具包
Solr:基于Lucene开发的企业级的搜索引擎产品
Elasticsearch:基于Lucene开发的企业级的搜索引擎产品
创建索引的流程
文档Document:数据库中一条具体的记录
字段Field:数据库中的每个字段
目录对象Directory:物理存储位置
写出器的配置对象:需要分词器和lucene的版本
Document(文档类)
Document:文档对象,是一条原始的数据
Field(字段类)
一个Document中可以有很多个不同的字段,每一个字段都是一个Field类的对象。
一个Document中的字段其类型是不确定的,因此Field类就提供了各种不同的子类,来对应这些不同类型的字段。
这些子类有一些不同的特性:
1)DoubleField、FloatField、IntField、LongField、StringField、TextField这些子类一定会被创建索引,但是不会被分词,而且不一定会被存储到文档列表。要通过构造函数中的参数Store来指定:如果Store.YES代表存储,Store.NO代表不存储
TextField即创建索引,又会被分词。StringField会创建索引,但是不会被分词。
如果不分词,会造成整个字段作为一个词条,除非用户完全匹配,否则搜索不到:
我们一般,需要搜索的字段,都会做分词:
StoreField一定会被存储,但是一定不创建索引
StoredField可以创建各种数据类型的字段:
问题1:如何确定一个字段是否需要存储?
如果一个字段要显示到最终的结果中,那么一定要存储,否则就不存储
问题2:如何确定一个字段是否需要创建索引?
如果要根据这个字段进行搜索,那么这个字段就必须创建索引。
问题3:如何确定一个字段是否需要分词?
前提是这个字段首先要创建索引。然后如果这个字段的值是不可分割的,那么就不需要分词。例如:ID
Directory(目录类)
指定索引要存储的位置
FSDirectory:文件系统目录,会把索引库指向本地磁盘。
特点:速度略慢,但是比较安全
RAMDirectory:内存目录,会把索引库保存在内存。
特点:速度快,但是不安全
Analyzer(分词器类)
• 提供分词算法,可以把文档中的数据按照算法分词
这些分词器,并没有合适的中文分词器,因此一般我们会用第三方提供的分词器:
IK分词器(重要)
- 概述
- 基本使用
引入IK分词器:
<dependency> <groupId>com.jianggujin</groupId> <artifactId>IKAnalyzer-lucene</artifactId> <version>8.0.0</version> </dependency>
- 扩展词典和停用词典
IK分词器的词库有限,新增加的词条可以通过配置文件添加到IK的词库中,也可以把一些不用的词条去除:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IKAnalyzer配置</comment> <entry key="ext_words">extwords.dic;</entry> <entry key="stop_words">stopwords.dic;</entry> </properties>
IndexWriterConfig(索引写出器配置类)
1) 设置配置信息:Lucene的版本和分词器类型
2)设置是否清空索引库中的数据
IndexWriter(索引写出器类)
- 索引写出工具,作用就是 实现对索引的增(创建索引)、删(删除索引)、改(修改索引)
// 批量创建索引 @Test public void testCreate2() throws Exception{ // 创建文档的集合 Collection<Document> docs = new ArrayList<>(); // 创建文档对象 Document document1 = new Document(); document1.add(new StringField("id", "1", Field.Store.YES)); document1.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES)); docs.add(document1); // 创建文档对象 Document document2 = new Document(); document2.add(new StringField("id", "2", Field.Store.YES)); document2.add(new TextField("title", "谷歌地图之父加盟FaceBook", Field.Store.YES)); docs.add(document2); // 创建文档对象 Document document3 = new Document(); document3.add(new StringField("id", "3", Field.Store.YES)); document3.add(new TextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Field.Store.YES)); docs.add(document3); // 创建文档对象 Document document4 = new Document(); document4.add(new StringField("id", "4", Field.Store.YES)); document4.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Field.Store.YES)); docs.add(document4); // 创建文档对象 Document document5 = new Document(); document5.add(new StringField("id", "5", Field.Store.YES)); document5.add(new TextField("title", "谷歌地图之父拉斯加盟社交网站Facebook", Field.Store.YES)); docs.add(document5); // 索引目录类,指定索引在硬盘中的位置 Directory directory = FSDirectory.open(new File("d:\indexDir")); // 引入IK分词器 Analyzer analyzer = new IKAnalyzer(); // 索引写出工具的配置对象 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer); // 设置打开方式:OpenMode.APPEND 会在索引库的基础上追加新索引。OpenMode.CREATE会先清空原来数据,再提交新的索引 conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE); // 创建索引的写出工具类。参数:索引的目录和配置信息 IndexWriter indexWriter = new IndexWriter(directory, conf); // 把文档集合交给IndexWriter indexWriter.addDocuments(docs); // 提交 indexWriter.commit(); // 关闭 indexWriter.close(); }
查询索引数据
实现步骤:
//1 创建读取目录对象
//2 创建索引读取工具
//3 创建索引搜索工具
//4 创建查询解析器
//5 创建查询对象
//6 搜索数据
//7 各种操作
@Test public void testSearch() throws Exception { // 索引目录对象 Directory directory = FSDirectory.open(new File("d:\indexDir")); // 索引读取工具 IndexReader reader = DirectoryReader.open(directory); // 索引搜索工具 IndexSearcher searcher = new IndexSearcher(reader); // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器 QueryParser parser = new QueryParser("title", new IKAnalyzer()); // 创建查询对象 Query query = parser.parse("谷歌"); // 搜索数据,两个参数:查询条件对象要查询的最大结果条数 // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。 TopDocs topDocs = searcher.search(query, 10); // 获取总条数 System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据"); // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分 ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 取出文档编号 int docID = scoreDoc.doc; // 根据编号去找文档 Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); // 取出文档得分 System.out.println("得分: " + scoreDoc.score); } }
QueryParser(查询解析器)
1)QueryParser(单一字段的查询解析器)
2)MultiFieldQueryParser(多字段的查询解析器)
Query(查询对象,包含要查询的关键词信息)
- 1)通过QueryParser解析关键字,得到查询对象
- 2)自定义查询对象(高级查询)
我们可以通过Query的子类,直接创建查询对象,实现高级查询(后面详细讲)
IndexSearch(索引搜索对象,执行搜索功能)
IndexSearch可以帮助我们实现:快速搜索、排序、打分等功能。
IndexSearch需要依赖IndexReader类
查询后得到的结果,就是打分排序后的前N名结果。N可以通过第2个参数来指定:
TopDocs(查询结果对象)
通过IndexSearcher对象,我们可以搜索,获取结果:TopDocs对象
在TopDocs中,包含两部分信息:
ScoreDoc(得分文档对象)
ScoreDoc是得分文档对象,包含两部分数据:
特殊查询
抽取公用的搜索方法:
public void search(Query query) throws Exception { // 索引目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 索引读取工具 IndexReader reader = DirectoryReader.open(directory); // 索引搜索工具 IndexSearcher searcher = new IndexSearcher(reader); // 搜索数据,两个参数:查询条件对象要查询的最大结果条数 // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。 TopDocs topDocs = searcher.search(query, 10); // 获取总条数 System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据"); // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分 ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 取出文档编号 int docID = scoreDoc.doc; // 根据编号去找文档 Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); // 取出文档得分 System.out.println("得分: " + scoreDoc.score); } }
TermQuery(词条查询)
/* * 测试普通词条查询 * 注意:Term(词条)是搜索的最小单位,不可再分词。值必须是字符串! */ @Test public void testTermQuery() throws Exception { // 创建词条查询对象 Query query = new TermQuery(new Term("title", "谷歌地图")); search(query); }
WildcardQuery(通配符查询)
/* * 测试通配符查询 * ? 可以代表任意一个字符 * * 可以任意多个任意字符 */ @Test public void testWildCardQuery() throws Exception { // 创建查询对象 Query query = new WildcardQuery(new Term("title", "*歌*")); search(query); }
FuzzyQuery(模糊查询)
/* * 测试模糊查询 */ @Test public void testFuzzyQuery() throws Exception { // 创建模糊查询对象:允许用户输错。但是要求错误的最大编辑距离不能超过2 // 编辑距离:一个单词到另一个单词最少要修改的次数 facebool --> facebook 需要编辑1次,编辑距离就是1 // Query query = new FuzzyQuery(new Term("title","fscevool")); // 可以手动指定编辑距离,但是参数必须在0~2之间 Query query = new FuzzyQuery(new Term("title","facevool"),1); search(query); }
NumericRangeQuery(数值范围查询)
/* * 测试:数值范围查询 * 注意:数值范围查询,可以用来对非String类型的ID进行精确的查找 */ @Test public void testNumericRangeQuery() throws Exception{ // 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值 Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true); search(query); }
BooleanQuery(组合查询)
/* * 布尔查询: * 布尔查询本身没有查询条件,可以把其它查询通过逻辑运算进行组合! * 交集:Occur.MUST + Occur.MUST * 并集:Occur.SHOULD + Occur.SHOULD * 非:Occur.MUST_NOT */ @Test public void testBooleanQuery() throws Exception{ Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true); Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true); // 创建布尔查询的对象 BooleanQuery query = new BooleanQuery(); // 组合其它查询 query.add(query1, BooleanClause.Occur.MUST_NOT); query.add(query2, BooleanClause.Occur.SHOULD); search(query); }
修改索引
步骤:
//1 创建文档存储目录
//2 创建索引写入器配置对象 //3 创建索引写入器 //4 创建文档数据 //5 修改 //6 提交 //7 关闭
/* 测试:修改索引 * 注意: * A:Lucene修改功能底层会先删除,再把新的文档添加。 * B:修改功能会根据Term进行匹配,所有匹配到的都会被删除。这样不好 * C:因此,一般我们修改时,都会根据一个唯一不重复字段进行匹配修改。例如ID * D:但是词条搜索,要求ID必须是字符串。如果不是,这个方法就不能用。 * 如果ID是数值类型,我们不能直接去修改。可以先手动删除deleteDocuments(数值范围查询锁定ID),再添加。 */ @Test public void testUpdate() throws Exception{ // 创建目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建配置对象 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer()); // 创建索引写出工具 IndexWriter writer = new IndexWriter(directory, conf); // 创建新的文档数据 Document doc = new Document(); doc.add(new StringField("id","1",Store.YES)); doc.add(new TextField("title","谷歌地图之父跳槽facebook ",Store.YES)); /* 修改索引。参数: * 词条:根据这个词条匹配到的所有文档都会被修改 * 文档信息:要修改的新的文档数据 */ writer.updateDocument(new Term("id","1"), doc); // 提交 writer.commit(); // 关闭 writer.close(); }
删除索引
步骤:
//1 创建文档对象目录
//2 创建索引写入器配置对象
//3 创建索引写入器
//4 删除
//5 提交
//6 关闭
/* * 演示:删除索引 * 注意: * 一般,为了进行精确删除,我们会根据唯一字段来删除。比如ID * 如果是用Term删除,要求ID也必须是字符串类型! */ @Test public void testDelete() throws Exception { // 创建目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建配置对象 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer()); // 创建索引写出工具 IndexWriter writer = new IndexWriter(directory, conf); // 根据词条进行删除 // writer.deleteDocuments(new Term("id", "1")); // 根据query对象删除,如果ID是数值类型,那么我们可以用数值范围查询锁定一个具体的ID // Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true); // writer.deleteDocuments(query); // 删除所有 writer.deleteAll(); // 提交 writer.commit(); // 关闭 writer.close(); }
高亮显示
原理:
1)给所有关键字加上一个HTML标签
给这个特殊的标签设置CSS样式
//1 创建目录 对象 //2 创建索引读取工具 //3 创建索引搜索工具 //4 创建查询解析器 //5 创建查询对象 //6 创建格式化器 //7 创建查询分数工具 //8 准备高亮工具 //9 搜索 //10 获取结果 //11 用高亮工具处理普通的查询结果
// 高亮显示 @Test public void testHighlighter() throws Exception { // 目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建读取工具 IndexReader reader = DirectoryReader.open(directory); // 创建搜索工具 IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser("title", new IKAnalyzer()); Query query = parser.parse("谷歌地图"); // 格式化器 Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>"); QueryScorer scorer = new QueryScorer(query); // 准备高亮工具 Highlighter highlighter = new Highlighter(formatter, scorer); // 搜索 TopDocs topDocs = searcher.search(query, 10); System.out.println("本次搜索共" + topDocs.totalHits + "条数据"); ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 获取文档编号 int docID = scoreDoc.doc; Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); String title = doc.get("title"); // 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值 String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title); System.out.println("title: " + hTitle); // 获取文档的得分 System.out.println("得分:" + scoreDoc.score); } }
排序
// 排序 @Test public void testSortQuery() throws Exception { // 目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建读取工具 IndexReader reader = DirectoryReader.open(directory); // 创建搜索工具 IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser("title", new IKAnalyzer()); Query query = parser.parse("谷歌地图"); // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序 Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true)); // 搜索 TopDocs topDocs = searcher.search(query, 10,sort); System.out.println("本次搜索共" + topDocs.totalHits + "条数据"); ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 获取文档编号 int docID = scoreDoc.doc; Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); } }
分页
// 分页 @Test public void testPageQuery() throws Exception { // 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数: int pageSize = 2;// 每页条数 int pageNum = 3;// 当前页码 int start = (pageNum - 1) * pageSize;// 当前页的起始条数 int end = start + pageSize;// 当前页的结束条数(不能包含) // 目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建读取工具 IndexReader reader = DirectoryReader.open(directory); // 创建搜索工具 IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser("title", new IKAnalyzer()); Query query = parser.parse("谷歌地图"); // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序 Sort sort = new Sort(new SortField("id", Type.LONG, false)); // 搜索数据,查询0~end条 TopDocs topDocs = searcher.search(query, end,sort); System.out.println("本次搜索共" + topDocs.totalHits + "条数据"); ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (int i = start; i < end; i++) { ScoreDoc scoreDoc = scoreDocs[i]; // 获取文档编号 int docID = scoreDoc.doc; Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); } }
得分算法
l Lucene会对搜索结果打分,用来表示文档数据与词条关联性的强弱,得分越高,表示查询的匹配度就越高,排名就越靠前!其算法公式是:
https://blog.csdn.net/weixin_42633131/article/details/82873731