封装的搜索引擎之lucene篇(转)

最近利用晚上下班还有周末的时间自己捣腾的封装了一个我自己的搜索引擎(基于lucene和solr).现在将大概的思路给写出来,分享下:

1.首先是索引对象,也可以说是查询的VO对象.封装了几个常用字段(如:主键,所属者ID,所属者姓名,进入详情页面的link,创建时间等),其他各个模块的字段(如:标题,内容,邮箱等)

SearchBean.java

字段的代码如下:

 1/********以下 共有字段***********/
2    /**
3     * 检索的内容
4     */
5    protected String keyword;
6    /**
7     * 拥有者ID
8     */
9    protected String owerId;
10    /**
11     * 拥有者name
12     */
13    protected String owerName;
14    /**
15     * 检索对象的唯一标识位的值
16     */
17    protected String id;
18    /**
19     * 检索出对象后进入详情页面的链接
20     */
21    protected String link;
22    /**
23     * 创建时间
24     */
25    protected String createDate;
26    /**
27     * index类型
28     */
29    protected String indexType;
30
31    //setter,getter方法省略
32/********以上 共有字段***********/
33
34/*************以下 其他字段************/
35    /**
36     * 需要检索出来的字段及其值的对应map
37     */
38    private Map<String, String> searchValues;
39
40    /**
41     * 值对象
42     */
43    private Object object;
44
45    /**
46     * 获取检索出来的doIndexFields字段的值
47     *
48     * @return
49     */
50    public Map<String, String> getSearchValues() {
51        return searchValues;
52    }
53
54    /**
55     * 设置检索出来的doIndexFields字段的值
56     *
57     * @param searchValues
58     */
59    public void setSearchValues(Map<String, String> searchValues) {
60        this.searchValues = searchValues;
61    }
62    /********************以上 其他字段*******************/<span></span>

抽象方法代码如下:

 1/*****************以下 抽象方法******************/
2    /**
3     * 返回需要进行检索的字段
4     *
5     * @return
6     */
7    public abstract String[] getDoSearchFields();
8
9    /**
10     * 进行索引的字段
11     *
12     * @return
13     */
14    public abstract String[] getDoIndexFields();
15
16    /**
17     * 初始化searchBean中的公共字段(每个对象都必须创建的索引字段)
18     * @throws Exception
19     */
20    public abstract void initPublicFields() throws Exception;
21
22    /**
23     * 返回索引类型
24     *
25     * @return
26     */
27    public abstract String getIndexType();
28    /*****************以上 抽象方法********************/

共有的方法:

 1/*******************以下 公共方法**********************/
2    /**
3     * 获取需要创建索引字段的键值对map
4     *
5     * @return
6     */
7    public Map<String, String> getIndexFieldValues() {
8        if(this.object == null){
9            logger.warn(“given object is null!”);
10            return Collections.emptyMap();
11        }
12
13        String[] doIndexFields = this.getDoIndexFields();
14        if(doIndexFields == null || doIndexFields.length < 1){
15            logger.debug(“given no doIndexFields!”);
16            return Collections.emptyMap();
17        }
18
19        Map<String, String> extInfo = new HashMap<String, String>();
20        for(String f : doIndexFields){
21            String value = getValue(f, object);
22            extInfo.put(f, value);
23        }
24
25        return extInfo;
26    }
27
28    /**
29     * 获取一个对象中的某个字段的值,结果转化成string类型
30     *
31     * @param field         字段名称
32     * @param obj           对象
33     * @return
34     */
35    private String getValue(String field, Object obj){
36        if(StringUtils.isEmpty(field)){
37            logger.warn(“field is empty!”);
38            return StringUtils.EMPTY;
39        }
40
41        String result = StringUtils.EMPTY;
42        try {
43            Object value = ObjectUtils.getFieldValue(object, field);
44            if (value == null)
45                result = StringUtils.EMPTY;
46            else if (value instanceof String)
47                result = (String) value;
48            else if (value instanceof Collections || value instanceof Map)
49                result = ToStringBuilder.reflectionToString(object);
50            else if (value instanceof Date)
51                result = DateUtils.formatDate((Date) value);
52            else
53                result = value.toString();
54
55        } catch (IllegalAccessException e) {
56            logger.error(“can not find a value for field ‘{}’ in object class ‘{}’!”, field, object.getClass());
57        }
58
59        return result;
60    }
61
62    /**
63     * you must use this method when you create the index, set what object you will to be created its index!
64     *
65     * @param object            the object which you will want to be create index
66     */
67    public void setObject(Object object){
68        this.object = object;
69    }
70
71    /**
72     * get what object you want to be created index!
73     *
74     * @return
75     */
76    public Object getObject(){
77        return this.object;
78    }
79    /***************以上 公共方法*************/

2.现在有很多开源或者闭源的索引引擎可以用在项目上使用,所以我写了一个接口和一个抽取了一些公共方法的抽象类,只需要将你选择的搜索引擎的具体创建索引,检索等功能的实现代码写在一个继承上面这个抽象类的子类中,就可以随意的切换使用的目标引擎.贴上接口和抽象类

SearchEngine.java

 

  1package com.message.base.search.engine;
2
3import com.message.base.pagination.PaginationSupport;
4import com.message.base.search.SearchBean;
5
6import java.util.List;
7
8/**
9 * 索引引擎实现构建索引.删除索引.更新索引.检索等操作.
10 *
11 * @author sunhao(sunhao.java@gmail.com)
12 * @version V1.0
13 * @createTime 13-5-5 上午1:38
14 */
15public interface SearchEngine {
16
17    /**
18     * 创建索引(考虑线程安全)
19     *
20     * @param searchBeans       对象
21     * @throws Exception
22     */
23    public void doIndex(List<SearchBean> searchBeans) throws Exception;
24
25    /**
26     * 删除索引
27     *
28     * @param bean              对象
29     * @throws Exception
30     */
31    public void deleteIndex(SearchBean bean) throws Exception;
32
33    /**
34     * 删除索引(删除多个)
35     *
36     * @param beans             对象
37     * @throws Exception
38     */
39    public void deleteIndexs(List<SearchBean> beans) throws Exception;
40
41    /**
42     * 进行检索
43     *
44     * @param bean              检索对象(一般只需要放入值keyword,即用来检索的关键字)
45     * @param isHighlighter     是否高亮
46     * @param start             开始值
47     * @param num               偏移量
48     * @return
49     * @throws Exception
50     */
51    public PaginationSupport doSearch(SearchBean bean, boolean isHighlighter, int start, int num) throws Exception;
52
53    /**
54     * 进行多个检索对象的检索
55     *
56     * @param beans             多个检索对象(一般只需要放入值keyword,即用来检索的关键字)
57     * @param isHighlighter     是否高亮
58     * @param start             开始值
59     * @param num               偏移量
60     * @return
61     * @throws Exception
62     */
63    public PaginationSupport doSearch(List<SearchBean> beans, boolean isHighlighter, int start, int num) throws Exception;
64
65    /**
66     * 删除某个类型的所有索引(考虑线程安全)
67     *
68     * @param clazz             索引类型
69     * @throws Exception
70     */
71    public void deleteIndexsByIndexType(Class<? extends SearchBean> clazz) throws Exception;
72
73    /**
74     * 删除某个类型的所有索引(考虑线程安全)
75     *
76     * @param indexType         索引类型
77     * @throws Exception
78     */
79    public void deleteIndexsByIndexType(String indexType) throws Exception;
80
81    /**
82     * 删除所有的索引
83     *
84     * @throws Exception
85     */
86    public void deleteAllIndexs() throws Exception;
87
88    /**
89     * 更新索引
90     *
91     * @param searchBean        需要更新的bean
92     * @throws Exception
93     */
94    public void updateIndex(SearchBean searchBean) throws Exception;
95
96    /**
97     * 批量更新索引
98     *
99     * @param searchBeans       需要更新的beans
100     * @throws Exception
101     */
102    public void updateIndexs(List<SearchBean> searchBeans) throws Exception;
103}

AbstractSearchEngine.java

 1package com.message.base.search.engine;
2
3import com.message.base.pagination.PaginationSupport;
4import com.message.base.pagination.PaginationUtils;
5import com.message.base.search.SearchBean;
6import com.message.base.utils.StringUtils;
7import org.slf4j.Logger;
8import org.slf4j.LoggerFactory;
9
10import java.util.Collections;
11
12/**
13 * 搜索引擎的公用方法.
14 *
15 * @author sunhao(sunhao.java@gmail.com)
16 * @version V1.0
17 * @createTime 13-5-8 下午10:53
18 */
19public abstract class AbstractSearchEngine implements SearchEngine {
20    private static final Logger logger = LoggerFactory.getLogger(AbstractSearchEngine.class);
21
22    /**
23     * 进行高亮处理时,html片段的前缀
24     */
25    private String htmlPrefix = “<p>”;
26    /**
27     * 进行高亮处理时,html片段的后缀
28     */
29    private String htmlSuffix = “</p>”;
30
31    public String getHtmlPrefix() {
32        return htmlPrefix;
33    }
34
35    public void setHtmlPrefix(String htmlPrefix) {
36        this.htmlPrefix = htmlPrefix;
37    }
38
39    public String getHtmlSuffix() {
40        return htmlSuffix;
41    }
42
43    public void setHtmlSuffix(String htmlSuffix) {
44        this.htmlSuffix = htmlSuffix;
45    }
46
47    public PaginationSupport doSearch(SearchBean bean, boolean isHighlighter, int start, int num) throws Exception {
48        if(bean == null){
49            logger.debug(“given search bean is empty!”);
50            return PaginationUtils.getNullPagination();
51        }
52
53        return doSearch(Collections.singletonList(bean), isHighlighter, start, num);
54    }
55
56    /**
57     * 获取index类型
58     *
59     * @param bean
60     * @return
61     */
62    public String getIndexType(SearchBean bean){
63        return StringUtils.isNotEmpty(bean.getIndexType()) ? bean.getIndexType() : bean.getClass().getSimpleName();
64    }
65}

3.开始谈谈lucene

贴上代码先:

LuceneSearchEngine.java

 

  1package com.message.base.search.engine;
2
3import com.message.base.pagination.PaginationSupport;
4import com.message.base.pagination.PaginationUtils;
5import com.message.base.search.SearchBean;
6import com.message.base.search.SearchInitException;
7import com.message.base.utils.StringUtils;
8import org.apache.lucene.analysis.Analyzer;
9import org.apache.lucene.analysis.SimpleAnalyzer;
10import org.apache.lucene.document.Document;
11import org.apache.lucene.document.Field;
12import org.apache.lucene.index.IndexReader;
13import org.apache.lucene.index.IndexWriter;
14import org.apache.lucene.index.Term;
15import org.apache.lucene.queryParser.MultiFieldQueryParser;
16import org.apache.lucene.search.BooleanClause;
17import org.apache.lucene.search.IndexSearcher;
18import org.apache.lucene.search.Query;
19import org.apache.lucene.search.ScoreDoc;
20import org.apache.lucene.search.highlight.Highlighter;
21import org.apache.lucene.search.highlight.QueryScorer;
22import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
23import org.apache.lucene.store.Directory;
24import org.apache.lucene.store.FSDirectory;
25import org.apache.lucene.util.Version;
26import org.slf4j.Logger;
27import org.slf4j.LoggerFactory;
28import org.springframework.beans.BeanUtils;
29
30import java.io.File;
31import java.io.IOException;
32import java.util.*;
33
34/**
35 * 基于lucene实现的索引引擎.
36 *
37 * @author sunhao(sunhao.java@gmail.com)
38 * @version V1.0
39 * @createTime 13-5-5 上午10:38
40 */
41public class LuceneSearchEngine extends AbstractSearchEngine {
42    private static final Logger logger = LoggerFactory.getLogger(LuceneSearchEngine.class);
43    /**
44     * 索引存放路径
45     */
46    private String indexPath;
47    /**
48     * 分词器
49     */
50    private Analyzer analyzer = new SimpleAnalyzer();
51
52    public synchronized void doIndex(List<SearchBean> searchBeans) throws Exception {
53        this.createOrUpdateIndex(searchBeans, true);
54    }
55
56    public synchronized void deleteIndex(SearchBean bean) throws Exception {
57        if(bean == null){
58            logger.warn(“Get search bean is empty!”);
59            return;
60        }
61
62        String id = bean.getId();
63
64        if(StringUtils.isEmpty(id)){
65            logger.warn(“get id and id value from bean is empty!”);
66            return;
67        }
68        String indexType = getIndexType(bean);
69        Directory indexDir = this.getIndexDir(indexType);
70        IndexWriter writer = this.getWriter(indexDir);
71
72        writer.deleteDocuments(new Term(“pkId”, id));
73        writer.commit();
74        this.destroy(writer);
75    }
76
77    public synchronized void deleteIndexs(List<SearchBean> beans) throws Exception {
78        if(beans == null){
79            logger.warn(“Get beans is empty!”);
80            return;
81        }
82
83        for(SearchBean bean : beans){
84            this.deleteIndex(bean);
85        }
86    }
87
88    public PaginationSupport doSearch(List<SearchBean> beans, boolean isHighlighter, int start, int num) throws Exception {
89        if(beans == null || beans.isEmpty()){
90            logger.debug(“given search beans is empty!”);
91            return PaginationUtils.getNullPagination();
92        }
93
94        List queryResults = new ArrayList();
95        int count = 0;
96        for(SearchBean bean : beans){
97            String indexType = getIndexType(bean);
98
99            IndexReader reader = IndexReader.open(this.getIndexDir(indexType));
100
101            List<String> fieldNames = new ArrayList<String>();             //查询的字段名
102            List<String> queryValue = new ArrayList<String>();             //待查询字段的值
103            List<BooleanClause.Occur> flags = new ArrayList<BooleanClause.Occur>();
104
105            //要进行检索的字段
106            String[] doSearchFields = bean.getDoSearchFields();
107            if(doSearchFields == null || doSearchFields.length == 0)
108                return PaginationUtils.getNullPagination();
109
110            //默认字段
111            if(StringUtils.isNotEmpty(bean.getKeyword())){
112                for(String field : doSearchFields){
113                    fieldNames.add(field);
114                    queryValue.add(bean.getKeyword());
115                    flags.add(BooleanClause.Occur.SHOULD);
116                }
117            }
118
119            Query query = MultiFieldQueryParser.parse(Version.LUCENE_CURRENT, queryValue.toArray(new String[]{}), fieldNames.toArray(new String[]{}),
120                    flags.toArray(new BooleanClause.Occur[]{}), analyzer);
121
122            logger.debug(“make query string is ‘{}’!”, query.toString());
123            IndexSearcher searcher = new IndexSearcher(reader);
124            ScoreDoc[] scoreDocs = searcher.search(query, 1000000).scoreDocs;
125
126            //查询起始记录位置
127            int begin = (start == -1 && num == -1) ? 0 : start;
128            //查询终止记录位置
129            int end = (start == -1 && num == -1) ? scoreDocs.length : Math.min(begin + num, scoreDocs.length);
130
131            //高亮处理
132            Highlighter highlighter = null;
133            if(isHighlighter){
134                SimpleHTMLFormatter formatter = new SimpleHTMLFormatter(this.getHtmlPrefix(), this.getHtmlSuffix());
135                highlighter = new Highlighter(formatter, new QueryScorer(query));
136            }
137
138            List<SearchBean> results = new ArrayList<SearchBean>();
139            for (int i = begin; i < end; i++) {
140                SearchBean result = BeanUtils.instantiate(bean.getClass());
141
142                int docID = scoreDocs[i].doc;
143                Document hitDoc = searcher.doc(docID);
144
145                result.setId(hitDoc.get(“pkId”));
146                result.setLink(hitDoc.get(“link”));
147                result.setOwerId(hitDoc.get(“owerId”));
148                result.setOwerName(hitDoc.get(“owerName”));
149                result.setCreateDate(hitDoc.get(“createDate”));
150                result.setIndexType(indexType);
151
152                String keyword = StringUtils.EMPTY;
153                if(isHighlighter && highlighter != null)
154                    keyword = highlighter.getBestFragment(analyzer, “keyword”, hitDoc.get(“keyword”));
155
156                if(StringUtils.isEmpty(keyword))
157                    keyword = hitDoc.get(“keyword”);
158
159                result.setKeyword(keyword);
160
161                Map<String, String> extendValues = new HashMap<String, String>();
162                for(String field : doSearchFields){
163                    String value = hitDoc.get(field);
164                    if(isHighlighter && highlighter != null)
165                        value = highlighter.getBestFragment(analyzer, field, hitDoc.get(field));
166
167                    if(StringUtils.isEmpty(value))
168                        value = hitDoc.get(field);
169
170                    extendValues.put(field, value);
171                }
172
173                result.setSearchValues(extendValues);
174
175                results.add(result);
176            }
177
178            queryResults.addAll(results);
179            count += scoreDocs.length;
180            searcher.close();
181            reader.close();
182        }
183
184        PaginationSupport paginationSupport = PaginationUtils.makePagination(queryResults, count, num, start);
185        return paginationSupport;
186    }
187
188    public synchronized void deleteIndexsByIndexType(Class<? extends SearchBean> clazz) throws Exception {
189        String indexType = getIndexType(BeanUtils.instantiate(clazz));
190        this.deleteIndexsByIndexType(indexType);
191    }
192
193    public synchronized void deleteIndexsByIndexType(String indexType) throws Exception {
194        //传入readOnly的参数,默认是只读的
195        IndexReader reader = IndexReader.open(this.getIndexDir(indexType), false);
196        int result = reader.deleteDocuments(new Term(“indexType”, indexType));
197        reader.close();
198        logger.debug(“the rows of delete index is ‘{}’! index type is ‘{}’!”, result, indexType);
199    }
200
201    public synchronized void deleteAllIndexs() throws Exception {
202        File indexFolder = new File(this.indexPath);
203        if(indexFolder == null || !indexFolder.isDirectory()){
204            //不存在或者不是文件夹
205            logger.debug(“indexPath is not a folder! indexPath: ‘{}’!”, indexPath);
206            return;
207        }
208
209        File[] children = indexFolder.listFiles();
210        for(File child : children){
211            if(child == null || !child.isDirectory()) continue;
212
213            String indexType = child.getName();
214            logger.debug(“Get indexType is ‘{}’!”, indexType);
215
216            this.deleteIndexsByIndexType(indexType);
217        }
218    }
219
220    public void updateIndex(SearchBean searchBean) throws Exception {
221        this.updateIndexs(Collections.singletonList(searchBean));
222    }
223
224    public void updateIndexs(List<SearchBean> searchBeans) throws Exception {
225        this.createOrUpdateIndex(searchBeans, false);
226    }
227
228    /**
229     * 创建或者更新索引
230     *
231     * @param searchBeans       需要创建或者更新的对象
232     * @param isCreate          是否是创建索引;true创建索引,false更新索引
233     * @throws Exception
234     */
235    private synchronized void createOrUpdateIndex(List<SearchBean> searchBeans, boolean isCreate) throws Exception {
236        if(searchBeans == null || searchBeans.isEmpty()){
237            logger.debug(“do no index!”);
238            return;
239        }
240
241        Directory indexDir = null;
242        IndexWriter writer = null;
243        for(Iterator<SearchBean> it = searchBeans.iterator(); it.hasNext(); ){
244            SearchBean sb = it.next();
245            String indexType = getIndexType(sb);
246            if(sb == null){
247                logger.debug(“give SearchBean is null!”);
248                return;
249            }
250            boolean anotherSearchBean = indexDir != null && !indexType.equals(((FSDirectory) indexDir).getFile().getName());
251            if(indexDir == null || anotherSearchBean){
252                indexDir = this.getIndexDir(indexType);
253            }
254            if(writer == null || anotherSearchBean){
255                this.destroy(writer);
256                writer = this.getWriter(indexDir);
257            }
258
259            Document doc = new Document();
260
261            //初始化一些字段
262            sb.initPublicFields();
263            String id = sb.getId();
264
265            //主键的索引,不作为搜索字段,并且也不进行分词
266            Field idField = new Field(“pkId”, id, Field.Store.YES, Field.Index.NOT_ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
267            doc.add(idField);
268
269            logger.debug(“create id index for ‘{}’, value is ‘{}’! index is ‘{}’!”, new Object[]{“pkId”, id, idField});
270
271            String owerId = sb.getOwerId();
272            if(StringUtils.isEmpty(owerId)){
273                throw new SearchInitException(“you must give a owerId”);
274            }
275            Field owerId_ = new Field(“owerId”, owerId, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
276            doc.add(owerId_);
277
278            String owerName = sb.getOwerName();
279            if(StringUtils.isEmpty(owerName)){
280                throw new SearchInitException(“you must give a owerName”);
281            }
282            Field owerName_ = new Field(“owerName”, owerName, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
283            doc.add(owerName_);
284
285            String link = sb.getLink();
286            if(StringUtils.isEmpty(link)){
287                throw new SearchInitException(“you must give a link”);
288            }
289            Field link_ = new Field(“link”, link, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
290            doc.add(link_);
291
292            String keyword = sb.getKeyword();
293            if(StringUtils.isEmpty(keyword)){
294                throw new SearchInitException(“you must give a keyword”);
295            }
296            Field keyword_ = new Field(“keyword”, keyword, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
297            doc.add(keyword_);
298
299            String createDate = sb.getCreateDate();
300            if(StringUtils.isEmpty(createDate)){
301                throw new SearchInitException(“you must give a createDate”);
302            }
303            Field createDate_ = new Field(“createDate”, createDate, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
304            doc.add(createDate_);
305
306            //索引类型字段
307            Field indexType_ = new Field(“indexType”, indexType, Field.Store.YES, Field.Index.NOT_ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
308            doc.add(indexType_);
309
310            //进行索引的字段
311            String[] doIndexFields = sb.getDoIndexFields();
312            Map<String, String> indexFieldValues = sb.getIndexFieldValues();
313            if(doIndexFields != null && doIndexFields.length > 0){
314                for(String field : doIndexFields){
315                    Field extInfoField = new Field(field, indexFieldValues.get(field), Field.Store.YES, Field.Index.ANALYZED,
316                            Field.TermVector.WITH_POSITIONS_OFFSETS);
317
318                    doc.add(extInfoField);
319                }
320            }
321
322            if(isCreate)
323                writer.addDocument(doc);
324            else
325                writer.updateDocument(new Term(“pkId”, sb.getId()), doc);
326
327            writer.optimize();
328        }
329
330        this.destroy(writer);
331        logger.debug(“create or update index success!”);
332    }
333
334    public Directory getIndexDir(String suffix) throws Exception {
335        return FSDirectory.open(new File(indexPath + File.separator + suffix));
336    }
337
338    public IndexWriter getWriter(Directory indexDir) throws IOException {
339        return new IndexWriter(indexDir, analyzer, IndexWriter.MaxFieldLength.UNLIMITED);
340    }
341
342    public void destroy(IndexWriter writer) throws Exception {
343        if(writer != null)
344            writer.close();
345    }
346
347    public void setIndexPath(String indexPath) {
348        this.indexPath = indexPath;
349    }
350
351    public void setAnalyzer(Analyzer analyzer) {
352        this.analyzer = analyzer;
353    }
354
355}

关于如何使用lucene这里我就不再重复了,网上一大堆这方面的资料,有什么不懂得可以谷歌一下.下面谈谈我的一些想法,有不对的,尽管拍砖,来吧:

….

也没啥好说的,等想到再补充吧,就是觉得有一点比较操蛋,窝心:

1FSDirectory.open(new File(“D:\index\xxx”/**一个不存在的目录,或者是一个不是索引的目录**/));

使用上面一段取到索引Directory的时候,如果目录不存在会报错.可以有人认为这没什么,就是应该,我封装的这代码里面,确实对这玩意有要求的.

 

上面的SearchBean.java中有一个字段叫indexType,当没有指定的时候,默认为类名,如MessageSerarchBean,如果我没有对Message进行创建索引操作,在检索的时候就报错了.我得想想用什么方法给解决掉.

转载于:http://www.blogjava.net/sunhao-java/archive/2013/05/19/399477.html

标签