zoukankan      html  css  js  c++  java
  • 搜索引擎

    搜索数据的常用方式

    1、数据库模糊查询

    使用like.

    2、使用数据库全文索引

    mysql 使用ngram插件实现了全文索引功能,可以在指定字段中进行搜索。

    3、solrlucenees

    https://blog.csdn.net/u014209975/article/details/53263642

    lucene是什么:

    Lucene是简单而功能强大的基于Java的搜索库。它可以用于任何应用程序来搜索功能。 Lucene是开源项目。它是可扩展的,高性能的库用于索引和搜索几乎任何类型的文本。 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

      

  • 相关阅读:
    SQL数据库——存储过程
    常用命令
    八大排序算法
    Java 反射的理解
    Java 集合的理解(持续更新......)
    JAVA 用数组实现 ArrayList
    JVM 运行时的内存分配
    Java中的增强 for 循环 foreach
    Java 泛型
    《七》随机访问文件流
  • 原文地址:https://www.cnblogs.com/baldprogrammer/p/13934635.html
Copyright © 2011-2022 走看看