lucene的简单使用
来源:互联网 发布:道奇垂耳兔 知乎 编辑:程序博客网 时间:2024/06/04 18:33
项目中要存景点的模块化信息,用到lucene做倒排索引,为了加快读取速度,把模块化信息存到内存,在内存中存入一个正排的索引,即根据id来存储,再利用lucene做一个倒排索引,可以根据特定的查找条件快速查询出结果。
构建索引
首先从数据库读出需要的数据,在经过程序处理,制作内存中的缓存索引。
package com.qyer.lucene;import com.google.common.collect.Maps;import com.qyer.entity.Poi;import com.qyer.utils.ConcurrentUtils;import org.apache.commons.lang3.ArrayUtils;import org.apache.lucene.analysis.standard.StandardAnalyzer;import org.apache.lucene.document.Document;import org.apache.lucene.document.Field;import org.apache.lucene.document.IntPoint;import org.apache.lucene.document.NumericDocValuesField;import org.apache.lucene.document.StoredField;import org.apache.lucene.index.DirectoryReader;import org.apache.lucene.index.IndexReader;import org.apache.lucene.index.IndexWriter;import org.apache.lucene.index.IndexWriterConfig;import org.apache.lucene.search.BooleanQuery;import org.apache.lucene.search.IndexSearcher;import org.apache.lucene.search.Query;import org.apache.lucene.search.ScoreDoc;import org.apache.lucene.search.Sort;import org.apache.lucene.search.SortField;import org.apache.lucene.search.TopDocs;import org.apache.lucene.spatial.geopoint.document.GeoPointField;import org.apache.lucene.spatial.geopoint.search.GeoPointDistanceQuery;import org.apache.lucene.store.Directory;import org.apache.lucene.store.RAMDirectory;import java.io.IOException;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.concurrent.Callable;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.stream.Collectors;import java.util.stream.Stream;import static org.apache.lucene.search.BooleanClause.Occur.MUST;/** * Created by 20170220b on 2017/9/18. */public class PoiIndexer { private static final String FIELD_ID = "id"; private static final String FIELD_CITY_ID = "city_id"; private static final String FIELD_CATEGORY_ID = "category_id"; private static final String FIELD_OVERALL_RANK = "overall_rank"; private static final String FIELD_LOCATION = "location"; private Directory directory; private IndexWriter indexWriter; private IndexReader indexReader; private IndexSearcher searcher; private ExecutorService service = Executors.newFixedThreadPool(64); public static PoiIndexer getInstance() { return InnerHolder.INSTANCE; } private static class InnerHolder { private static final PoiIndexer INSTANCE = new PoiIndexer(); } private PoiIndexer() { this.directory = new RAMDirectory(); } public void buildWriter() throws IOException { this.indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer())); } public void buildReaderSearcher() throws IOException { this.indexReader = DirectoryReader.open(this.directory); this.searcher = new IndexSearcher(this.indexReader); } public void put(Poi poi) throws Exception { this.indexWriter.addDocument(poi2Doc(poi)); this.indexWriter.commit(); } public Document poi2Doc(Poi poi) { Document doc = new Document(); doc.add(new StoredField(FIELD_ID, poi.getId())); doc.add(new GeoPointField(FIELD_LOCATION, poi.getLat(), poi.getLng(), Field.Store.NO)); doc.add(new IntPoint(FIELD_CITY_ID, poi.getCityid())); doc.add(new IntPoint(FIELD_CATEGORY_ID, poi.getCateid())); doc.add(new NumericDocValuesField(FIELD_OVERALL_RANK, poi.getOverallRank())); return doc; } private int getIdField(int docId) { int id = Integer.MIN_VALUE; try { id = searcher.doc(docId).getField(FIELD_ID).numericValue().intValue(); } catch (IOException e) { e.printStackTrace(); } return id; } public List<Integer> query(double lat, double lng, double dist, List<Integer> cityId, int cateid, int limit) throws Exception { Map<Integer, Callable<List<Integer>>> callableMap = Maps.newHashMapWithExpectedSize(limit); if (cityId.contains(1)) { callableMap.put(1, () -> search(lat, lng, dist, 1, cateid, limit)); } if (cityId.contains(2)) { callableMap.put(2, () -> { return search(lat, lng, dist, 2, cateid, limit); }); } if (cityId.contains(3)) { callableMap.put(3, new Callable<List<Integer>>() { @Override public List<Integer> call() throws Exception { return PoiIndexer.this.search(lat, lng, dist, 3, cateid, limit); } }); } if (cityId.contains(4)) { callableMap.put(4, () -> search(lat, lng, dist, 4, cateid, limit)); } Map<Integer, List<Integer>> map = ConcurrentUtils.getFutureContentMap(service, callableMap, 3000); List<Integer> result = new ArrayList<>(); for (Integer key : map.keySet()) { result.addAll(map.get(key)); } //不采用多线程// List<Integer> result = new ArrayList<>();// if(cityId.contains(1)){// result.addAll(search(lat,lng,dist,1,cateid,limit));// }// if(cityId.contains(2)){// result.addAll(search(lat,lng,dist,2,cateid,limit));// }// if(cityId.contains(3)){// result.addAll(search(lat,lng,dist,3,cateid,limit));// }// if(cityId.contains(4)){// result.addAll(search(lat,lng,dist,4,cateid,limit));// } return result; } public List<Integer> search(double lat, double lng, double dist, int cityId, int cateids, int limit) throws IOException { List<Integer> result = new ArrayList<>(); BooleanQuery.Builder builder = new BooleanQuery.Builder(); builder.add(new GeoPointDistanceQuery(FIELD_LOCATION, lat, lng, dist), MUST) .add(IntPoint.newExactQuery(FIELD_CITY_ID, cityId), MUST) .add(IntPoint.newExactQuery(FIELD_CATEGORY_ID, cateids), MUST); SortField sortField = new SortField(FIELD_OVERALL_RANK, SortField.Type.INT, false); Query query = builder.build(); TopDocs hits = searcher.search(query, limit, new Sort(sortField)); ScoreDoc[] docs = hits.scoreDocs; if (ArrayUtils.isNotEmpty(docs)) { result.addAll(Stream.of(docs).map(scoreDoc -> getIdField(scoreDoc.doc)).collect(Collectors.toList())); } return result; } public void stopWrite() throws IOException { if (this.indexWriter != null) { this.indexWriter.close(); } } public void stopRead() throws IOException { if (this.indexReader != null) { this.indexReader.close(); } }}
项目建立在spring-boot上,把缓存的倒排类做成单例,以供实例使用。类中有私有的成员变量,directory是目录类,也就是我们要维护的缓存。indexWriter是构建索引,写入内容的操作对象。indexReader是在索引构建好了之后,实例读取索引的操作对象,searcher是查询的对象。
首先在初始化程序中,调用方法构造方法,在初始化方法里面初始化了directory,初始化类型为内存型的,也可以初始化为文件类型,就是索引缓存存在的位置的区别,然后调用buildWriter方法,初始化indexWriter变量,用来构建索引。
@Service@Order(value = 0)public class PoiService implements CommandLineRunner{ private PoiIndexer poiIndexer = PoiIndexer.getInstance(); private PoiStorage poiStorage = PoiStorage.getInstance(); @SuppressWarnings("SpringJavaAutowiringInspection") @Autowired private PoiDAO poiDAO; @Override public void run(String... strings) throws Exception { poiIndexer.buildWriter(); List<Poi> list = poiDAO.selectAll(); for (Poi poi : list) { poiIndexer.put(poi); poiStorage.put(poi.getId(), poi); } poiIndexer.buildReaderSearcher(); poiIndexer.stopWrite(); }}
这里是调用的顺序,在调用构造方法和buildWriter后,在数据库接口查询出的数据循环中第阿勇put方法,进行填充数据。实例化一个Document文档,向里面加入各种索引的条件字段,包括排序字段等,然后调用buildReaderSearcher,实例化出search变量,用来全局实例进行查询操作。然后关闭indexWriter。这样数据库数据就存入了内存,以倒排的形式。
这里面核心的使用lucene的功能是GeoPointField,但是lucene本身并不是主打功能,使用案例也比较少,这里面由于业务上需要把位置信息做成索引,这里面有geo算法,可以直接使用,自己实现geo算法难度巨大。
使用索引
@RestControllerpublic class Controller { private PoiIndexer poiIndexer = PoiIndexer.getInstance(); private PoiStorage poiStorage = PoiStorage.getInstance(); private final List<Integer> cityAcceptList = Arrays.asList(1, 2, 3, 4); private final String DEFAULT_CITY = "1,2,3,4"; @RequestMapping("/poi") public List<Poi> poi(HttpServletRequest req) throws Exception { double lat = CommonUtils.cast2Double(req.getParameter("lat")); double lng = CommonUtils.cast2Double(req.getParameter("lng")); double dist = CommonUtils.cast2Double(req.getParameter("dist"), 3000d); int limit = CommonUtils.cast2Int(req.getParameter("limit"), 20); int cateId = CommonUtils.cast2Int(req.getParameter("cate_id"), 1); List<Integer> cityList = StringToList.stringToList(req.getParameter("city_id"), cityAcceptList, DEFAULT_CITY); long t1 = System.currentTimeMillis(); List<Integer> ids = null; for (int i = 0; i < 100000; i++) { ids = poiIndexer.query(lat, lng, dist, cityList, cateId, limit); } long t2 = System.currentTimeMillis(); System.out.println(t2 - t1); List<Poi> result = Lists.newArrayListWithExpectedSize(5); for (Integer id : ids) { result.add(poiStorage.get(id)); } return result; }}
接下来是使用缓存倒排索引的时候,通过调用query方法,由于有一参数条件是列表形式,做了一个转化处理,这里同事做了多线程的查询,以加快速度。query会消化查询条件,然后调用search方法,通过构建一个查询的条件BooleanQuery来进行条件的控制查询,其中三个条件都是与的关系。lucene做倒排索引的用法就是这样。
正排索引与倒排索引相似,但是相对简单很多,也是做成单例的类,维护一个map,与倒排索引初始化同步,向map里面存入数据,放到内存,然后提供一个方法对外使用。
多线程的Callable使用之前也有使用过,这里方法上封装了很多,首先在query方法中,通过查询条件的判断,向callableMap中填入任务,然后放入封装的方法中取得future结果。
public static <K, R> Map<K, R> getFutureContentMap(ExecutorService service, Map<K, Callable<R>> operatorMap, long timeoutInMilli) throws InvocationException { if (MapUtils.isEmpty(operatorMap)) { return null; } Map<K, Future<R>> futureMap = Maps.newHashMapWithExpectedSize(operatorMap.size()); operatorMap.forEach((k, c) -> futureMap.put(k, service.submit(c))); return getFutureContentMap(futureMap, timeoutInMilli); } public static <K, V> Map<K, V> getFutureContentMap(Map<K, Future<V>> futureMap, long timeoutInMilli) throws InvocationException { if (MapUtils.isEmpty(futureMap)) { return null; } Map<K, V> map = Maps.newHashMapWithExpectedSize(futureMap.size()); long t1, t2, timeUse = 0; K k; V v; Future<V> future; try { for (Map.Entry<K, Future<V>> entry : futureMap.entrySet()) { t1 = System.currentTimeMillis(); k = entry.getKey(); future = entry.getValue(); v = future.get(timeoutInMilli - timeUse, MILLISECONDS); map.put(k, v); t2 = System.currentTimeMillis(); timeUse += (t2 - t1); } } catch (Exception e) { throw new InvocationException(e); } return map; }
这个通用的方法是leader写的,加入了泛型比较通用。简单地说就是通过放入线程池中,通过submit方法取得future结果,做了很多的超时限制。
这里的添加到callableMap过程有三种写法,第一种是及其简单的写法,但是一开始理解起来有些难度,用了lambda写法。利用intellij的提示键,可以把第一种的写法自动拆成第二种写法(intellij的很多功能真的很好用),其实就是显示的return结果。然后继续提示键,就到了最原始的内部类的写法,就很通俗易懂了,通过重写call方法,执行search方法。为了测试,执行查询10万次,效率上可以提高3秒左右。
spring-boot关闭
到这里lucene的reader还没有关闭,大的程序上可能还有一些开关没有关闭,为了规范,都要在项目关闭之前关闭掉,spring提供了接口
@Componentpublic class Close { private PoiIndexer poiIndexer = PoiIndexer.getInstance(); @PreDestroy public void close() throws Exception { poiIndexer.stopRead(); }}
首先在类的上方@Component注入,然后在方法上 @PreDestroy注明,spring关闭程序的时候就会执行方法,这里面关闭掉reader,这个标签还没有找到类似于order之类的关闭顺序定义的方法,暂时只用一个方法去关闭资源,关闭的顺序就在方法内顺序执行了。
异常处理
对于spring-boot项目,如果项目中出了我们没有处理到的异常,就会返回一个spring的错误的页面,这对前段不是很友好。
@ControllerAdvicepublic class ExceptionController { private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionController.class); @ExceptionHandler(value = Exception.class) @ResponseBody public ExceptionInfo<String> error(Exception e) throws Exception { LOGGER.error("{}", e.fillInStackTrace()); if (e instanceof org.springframework.web.servlet.NoHandlerFoundException) { ExceptionInfo<String> exceptionInfo = new ExceptionInfo<>(404, e.getMessage()); return exceptionInfo; } else { ExceptionInfo<String> exceptionInfo = new ExceptionInfo<>(500, e.getMessage()); return exceptionInfo; } }}
通过@ControllerAdvice可以拦截这些异常,这里主要拦截404,500.自己定义一个异常返回的封装类,自定属性,可以与前端沟通好进行设置属性。在项目里需要自己捕获并处理的异常,这里并不影响,项目中处理特定异常还是按照正常的处理走。这个是拦截没有处理的异常,但是要加上两行配置。
#为处理异常页面404,500#出现错误时, 直接抛出异常spring.mvc.throw-exception-if-no-handler-found=true#不要为我们工程中的资源文件建立映射spring.resources.add-mappings=false
spring 有着很多的标签配置等来满足功能上需求,每次解决了问题都会感叹spring的强大。
- Lucene的简单使用
- Lucene的简单使用
- lucene的简单使用
- Lucene的简单使用
- Lucene的简单使用
- Lucene索引的简单使用
- 简单的lucene框架 Compass 的使用
- Lucene的使用以及简单介绍
- Lucene简单的使用配置详解
- 使用lucene实现简单的全文检索
- lucene简单使用
- Lucene简单使用
- lucene简单使用
- lucene的简单实例
- lucene的简单实例
- lucene的简单实例
- lucene的简单实例
- lucene 简单的例子
- iOS 图片虚化。裁剪。等比例缩放
- NYG的序列拆分 详解(递推矩阵快速幂)
- The JRE_HOME environment variable is not defined correctly This environment variable is needed 。。。
- OMRON E6B2-CWZ6C
- 常用类——字符
- lucene的简单使用
- Centos7 远程无法连接mysql数据库
- STM32F10在iap和app模式下,调试模式串口可以通信,下载后却不能通信的问题
- unity学习——值类型和引用类型
- git命令
- 七个结构型模式5:外观模式-Facade Pattern【学习难度:★☆☆☆☆,使用频率:★★★★★】
- NYOJ 33
- 杭电oj的2023
- 关于const修饰的问题【转载】