开源搜索引擎 - Lucene入门

Lucene是一个开源的搜索引擎, 或者叫全文索引工具, 用于快速查找, 而且可以获得一个匹配度得分;

比如我们直接使用sql进行搜索的时候, 可能会使用Like关键词

但是这样有两个不便之处, 一是当数据太多时, 会比较慢, 二是这样无法得到一些基于关键词的匹配相似度得分;

用lucene可以实现上面两点;

那么直接开始做一个简单的搜索的demo吧~

一.创建索引

假设现在已经有了一些数据, 放在了一个list里面:

List<String> productNames = new ArrayList<>();
productNames.add("飞利浦led灯泡e27螺口暖白球泡灯家用照明超亮节能灯泡转色温灯泡");
productNames.add("飞利浦led灯泡e14螺口蜡烛灯泡3W尖泡拉尾节能灯泡暖黄光源Lamp");
productNames.add("雷士照明 LED灯泡 e27大螺口节能灯3W球泡灯 Lamp led节能灯泡");
productNames.add("飞利浦 led灯泡 e27螺口家用3w暖白球泡灯节能灯5W灯泡LED单灯7w");
productNames.add("飞利浦led小球泡e14螺口4.5w透明款led节能灯泡照明光源lamp单灯");
productNames.add("飞利浦蒲公英护眼台灯工作学习阅读节能灯具30508带光源");
productNames.add("欧普照明led灯泡蜡烛节能灯泡e14螺口球泡灯超亮照明单灯光源");
productNames.add("欧普照明led灯泡节能灯泡超亮光源e14e27螺旋螺口小球泡暖黄家用");
productNames.add("聚欧普照明led灯泡节能灯泡e27螺口球泡家用led照明单灯超亮光源");
Directory index = createIndex(analyzer, productNames);

最后一行的createIndex方法实现如下:

private static Directory createIndex(IKAnalyzer analyzer, List<String> products) throws IOException {
    Directory index = new RAMDirectory();
    IndexWriterConfig config = new IndexWriterConfig(analyzer);
    IndexWriter writer = new IndexWriter(index, config);
 
    for (String name : products) {
        addDoc(writer, name);
    }
    writer.close();
    return index;
}

这里传入的参数有两个, 第一个是中文分词器, 第二个是存入的数据;

第一步是创建一个Dictionary, 然后构建一个IndexWriter用于写入, 这里要给这个IndexWritter使用一个Config因为需要使用中文分词器对传入的数据进行解析, 然后创建好writer之后循环写入数据即可, 这里的addDoc方法实现如下, 最后关闭writer即可:

private static void addDoc(IndexWriter w, String name) throws IOException {
    Document doc = new Document();
    doc.add(new TextField("name", name, Field.Store.YES));
    w.addDocument(doc);
}

在这里传入一个写入器;
其实这里是创建一个新的Document然后把这个document用writer写入到之前创建的dictionary里面, 此处的document是可以有多个Field的, 但是这里只传入了一项数据, 所以只需要用到一个TextField叫name;

然后到这里为止, 我们的数据部分就准备完成了, 然后下面我们开始执行一次查询

二.执行一次查询

先构建一个查询器

首先我们获取到用户提供的关键词, 然后根据这个关键词来构建一个查询器, 大概代码如下:

String keyword = "护眼带光源";
Query query = new QueryParser("name", analyzer).parse(keyword);

这里我们首先获得了一个keyword, 然后构造了一个QueryParser解析器, 然后传入一个analyzer(就是之前创建的中文分词器), 然后我们要对name进行搜索, 解析keyword, 这样一个搜索的查询器就构建好了;

然后进行搜索
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher = new IndexSearcher(reader);
int numberPerPage = 1000;
System.out.printf("当前一共有%d条数据%n",productNames.size());
System.out.printf("查询关键字是:\"%s\"%n",keyword);
ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;

首先要构造一个IndexReader对之前创建的Dictionary进行读取, 这里调用的是DirectoryReader的静态方法, 然后使用IndexSearcher来搜索, 设定每页要显示多少条数据, 然后调用searcher的search方法, 这里的返回的是一个scoreDoc的数组;

然后显示搜索结果

其实这部分就是要对刚才获得的结果, 也就是那个scoreDoc这个数组进行解析, 获得内容;
大概代码如下:

private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer)
        throws Exception {
    System.out.println("找到 " + hits.length + " 个命中.");
    System.out.println("序号\t匹配度得分\t结果");
    for (int i = 0; i < hits.length; ++i) {
        ScoreDoc scoreDoc= hits[i];
        int docId = scoreDoc.doc;
        Document d = searcher.doc(docId);
        List<IndexableField> fields = d.getFields();
        System.out.print((i + 1));
        System.out.print("\t" + scoreDoc.score);
        for (IndexableField f : fields) {
            System.out.print("\t" + d.get(f.name()));
        }
        System.out.println();
    }
}

可以看到, 这里直接取scoreDoc.doc获得的是在原来的Dictionary中的document id, 然后要利用searcher的doc方法来获得这个document的内容, 然后此处的document中只有一个field就是name, 但是往往可能会有多个field, 所以这里用了遍历所有的field来输出内容, 兼容性更好

整理

所以Lucene的基本流程是这样的:

第一步创建索引
获得数据 → 创建Dictionary
第二步开始查询
查询解析器处理关键词 → 查询器 → IndexSearcher(IndexReader) → ScoreDoc

一些花里胡哨的东西

1.分页查询
这个lucene可以使用分页查询, 可以看到, 在搜索这一步, 有这个操作

ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;

这一步就是把符合的hits的scoredoc全部存到了这个hits数组里面, 也就相当于保存到了内存中, 这样在数据量较大的时候是不太好的

所以可以使用分页查询, 比如每页10条数据, 一共20页, 我要查第7页的那10条数据

上面的方法就是, 我把这20页的200条数据全部存到内存中, 然后去读取我要的;

但是还可以我先查找到第69条数据, 然后使用searchAfter方法, 查找后面的10条数据, 这样就不用把所有数据都放到内存里了, 给个大概的demo把~

    private static ScoreDoc[] pageSearch2(Query query, IndexSearcher searcher, int pageNow, int pageSize)throws IOException{
        int start = (pageNow-1)*pageSize;
        if(start == 0){
            return searcher.search(query, pageSize).scoreDocs;
        }

        TopDocs topDocs = searcher.search(query, start);
        ScoreDoc preScoreDoc = topDocs.scoreDocs[start-1];
        topDocs = searcher.searchAfter(preScoreDoc, query, pageSize);
        return topDocs.scoreDocs;
    }

2.索引的增加删除修改
Dictionary在创建之后也是可以修改的

增加的方法就和前面创建的时候一样, 还是用writer来写进去add就好了;
删除可以用deleteDocument方法, 类似这样:

        //删除id=51173的数据
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        IndexWriter indexWriter = new IndexWriter(index, config);
        indexWriter.deleteDocuments(new Term("id", "51173"));
        indexWriter.commit();
        indexWriter.close();

然后lucene还支持其他的一些方式的删除, 比如这样的:

    DeleteDocuments(Query query):根据Query条件来删除单个或多个Document
    DeleteDocuments(Query[] queries):根据Query条件来删除单个或多个Document
    DeleteDocuments(Term term):根据Term来删除单个或多个Document
    DeleteDocuments(Term[] terms):根据Term来删除单个或多个Document
    DeleteAll():删除所有的Document

然后修改也差不多, demo给一个:

IndexWriterConfig config = new IndexWriterConfig(analyzer);
IndexWriter indexWriter = new IndexWriter(index, config);
Document doc = new Document();
doc.add(new TextField("id", "51173", Field.Store.YES));
doc.add(new TextField("name", "神鞭,鞭没了,神还在", Field.Store.YES));
doc.add(new TextField("category", "道具", Field.Store.YES));
doc.add(new TextField("price", "998", Field.Store.YES));
doc.add(new TextField("place", "南海群岛", Field.Store.YES));
doc.add(new TextField("code", "888888", Field.Store.YES));
indexWriter.updateDocument(new Term("id", "51173"), doc );
indexWriter.commit();
indexWriter.close();

更多内容可以到HOW2J网站学习

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章