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的强大。