基于Solr的空间搜索技术

245 阅读10分钟

写在前面

最近做了个地图相关的功能,需求如下:

:::color1 在微信小程序中依托腾讯地图实现查询附近职位,要求能够查询附近3km、5km、10km等职位数据(类似于Boss直聘中的“附近机会”功能)。

:::

看到这个需求,需要考虑以下几点功能:

  1. 地理空间查询:你的代码中涉及到地理坐标(GPSX 和 GPSY)的处理,并且有地理过滤的需求。
  2. 复杂查询:你的查询逻辑非常复杂,涉及多个字段的组合查询、范围查询、分页查询等。
  3. 数据更新频率:需要频繁地更新索引(例如添加、删除、修改记录)。
  4. 数据量大,查询效率问题。

对于几百万的职位数据量,直接读关系型数据库进行查询,速度方面大打折扣,用户体验肯定非常不好。遂考虑到使用Elasticsearch或者Solr。

Elasticsearch

  1. 地理空间查询:
  • Elasticsearch 提供了强大的地理空间查询功能,支持多种地理坐标格式(如 geo_point 和 geo_shape),并且内置了丰富的地理查询操作(如距离查询、多边形查询等)。
  1. 复杂查询:
  • Elasticsearch 支持复杂的布尔查询、嵌套查询、聚合查询等功能,可以很好地满足你当前的查询需求。
  1. 数据更新频率:
  • Elasticsearch 的批量更新和实时索引功能非常适合频繁的数据更新场景。
  1. 全文搜索:
  • Elasticsearch 在全文搜索方面表现优异,支持多种语言分析器和分词器。
  1. 性能:
  • Elasticsearch 在大规模数据集上的查询性能较好,特别是分布式环境下的扩展性非常好。
  1. 易用性:
  • Elasticsearch 的 RESTful API 设计简洁,易于集成和使用。
  • 配置管理相对简单,支持通过 Kibana 进行可视化管理。
  1. 社区支持:
  • Elasticsearch 拥有非常活跃的社区和丰富的文档资源。

Solr:

  1. 地理空间查询:
  • Solr 也支持地理空间查询,但配置和使用相对复杂一些,特别是对于复杂的地理查询。
  1. 复杂查询:
  • Solr 支持复杂的查询语法(如 Lucene 查询语法),但对于某些高级查询功能的支持不如 Elasticsearch 完善。
  1. 数据更新频率:
  • Solr 也支持批量更新和实时索引,但在高并发场景下的性能可能稍逊于 Elasticsearch。
  1. 全文搜索:
  • Solr 在全文搜索方面也有不错的表现,但相对于 Elasticsearch,在某些高级特性上略显不足。
  1. 性能:
  • Solr 在小规模数据集上的查询性能较好,但在大规模数据集和分布式环境下的扩展性不如 Elasticsearch。
  1. 易用性:
  • Solr 的配置较为复杂,特别是对于初学者来说,学习曲线较陡。
  • 配置管理相对繁琐,缺乏像 Kibana 这样的可视化工具。
  1. 社区支持:
  • Solr 的社区也很活跃,但相对于 Elasticsearch,其社区资源和文档稍微少一些。

综合以上分析,Elasticsearch 更适合实现你当前的功能需求:

  1. 地理空间查询:Elasticsearch 提供了更强大和灵活的地理空间查询功能。
  2. 复杂查询:Elasticsearch 的查询语法和功能更为丰富,能够更好地满足复杂的查询需求。
  3. 数据更新频率:Elasticsearch 在频繁数据更新场景下的性能更好。
  4. 全文搜索:Elasticsearch 在全文搜索方面的表现更为优异。
  5. 性能:Elasticsearch 在大规模数据集和分布式环境下的扩展性和查询性能更好。
  6. 易用性:Elasticsearch 的 API 更加简洁,配置管理更方便,拥有更好的可视化管理工具(如 Kibana)。
  7. 社区支持:Elasticsearch 拥有更活跃的社区和更丰富的文档资源。

so???以为我要使用Elasticsearch了吗?那这篇文章就不是关于Solr的了,由于long long ago ,公司这个平台对职位的搜索选用的是Solr(咱也不知道是基于什么原因),如果换成Elasticsearch就需要把涉及到的所有功能都要重构,没办法,智能在Solr的基础上实现这个需求了。所以有了这篇对Solr空间搜索技术的扫盲。

1. 概念

Solr 是一个基于 Lucene 的开源搜索平台,提供了强大的全文搜索、高性能、可扩展和分布式搜索功能。Solr 空间搜索(Spatial Search)是 Solr 的一个扩展功能,它允许用户基于地理位置信息进行搜索和过滤。空间搜索在许多领域都有广泛的应用,如地图服务、位置感知应用、推荐系统等。

Solr 空间搜索是指在 Solr 搜索引擎中,针对具有空间属性的数据进行搜索和查询的能力。这些空间属性可以是地理坐标(如经纬度),也可以是表示空间位置的其他几何形状,如点、线、面等。通过空间搜索,我们能够实现诸如查找某个区域内的所有店铺、检索距离某个地点一定范围内的兴趣点等功能。

2. Solr空间搜索的原理

Solr 空间搜索的核心原理是利用地理空间索引和查询来实现高效的地理位置相关搜索。Solr 支持多种地理空间数据类型,包括点(Point)、多边形(Polygon)和多段线(LineString)。Solr 使用 R-Tree 索引来存储和查询这些地理空间数据,R-Tree 是一种专门用于存储多维信息的树形数据结构,特别适合处理空间数据。

Solr 空间搜索主要基于两种常见的空间索引结构:R-Tree 和 Quad-Tree。

2.1 R-Tree

R-Tree 是一种用于存储多维空间数据的树形数据结构。在空间搜索中,R - Tree 将空间对象(如点、矩形等)按照其包围盒(能完全包含该对象的最小矩形)进行分组存储。每个节点包含多个条目,每个条目由一个包围盒和一个指向子节点或数据对象的指针组成。当进行查询时,从根节点开始,通过比较查询区域与节点中包围盒的重叠情况,快速排除不相关的分支,从而大大提高查询效率。

2.2 Quad-Tree

Quad-Tree 则是将二维空间递归地划分为四个象限。每个节点表示一个矩形区域,并且最多有四个子节点,分别对应四个象限。数据点根据其所在的象限被分配到相应的子节点中。在查询时,通过不断细分象限,找到包含查询点或与查询区域相交的节点,进而获取相关数据。

Solr 在处理空间搜索请求时,首先会将用户的查询条件(如坐标范围、几何形状等)转化为内部的查询表达式,然后利用上述空间索引结构快速定位到符合条件的数据。

3. 地理空间字段类型

Solr 提供了几种地理空间字段类型,其中最常用的是 PointType 和 LatLonType。PointType 用于存储二维坐标点,而 LatLonType 则专门用于存储经纬度坐标。这些字段类型允许 Solr 对地理空间数据进行索引和查询。

4. 查询语法

Solr 提供了多种空间查询语法,包括 BBox(Bounding Box)、Distance 和 Is Within 等。BBox 查询用于查找位于指定矩形区域内的文档;Distance 查询用于查找距离某个点一定距离范围内的文档;Is Within 查询用于查找位于某个多边形内的文档。

5. 实战中使用

  1. 配置

首先,需要在 Solr 的 schema.xml 文件中定义地理空间字段。我是直接在Windows上通过tomcat部署了Solr,通过地址http://localhost:9280/solr/index.html就可以访问solr的控制台,在控制台中直接添加的(对应的是managed-schema文件,而不是schema.xml文件)

如果在schema.xml 中可以这么配置:

<field name="location" type="location" indexed="true" stored="true" multiValued="false"/>
<fieldType name="location" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>

我直接在控制台图形化添加的:

反映在配置文件(managed-schema)中是这样的(由于数据库中存的是GPSX和GPSY,所以这里也添加了这两个字段,类型使用pdouble):

<fieldType name="location" class="solr.LatLonPointSpatialField" docValues="true"/>
<field name="location" type="location" uninvertible="true" indexed="true" stored="true"/>
<field name="GPSX" type="pdouble" uninvertible="true" indexed="true" stored="true"/>
<field name="GPSY" type="pdouble" uninvertible="true" indexed="true" stored="true"/>
<field name="id" type="string" multiValued="false" indexed="true" required="true" stored="true"/>

对应的java实体为:

package com.neusoft.lpleaf6.job.api.dto.v;


import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.annotation.Id;

import lombok.Data;

@Data
public class VJobSorlDataDto implements java.io.Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@Field("id")
	private String acb210;
	@Field("GPSX")
	private double gpsx;
	@Field("GPSY")
	private double gpsy;
	@Field("location")
	private String location;

}

  1. 构建索引
//SolrService是封装的一个Solr工具类
@Autowired
private SolrService solrService;

@Override
@Transactional
public void buildJobIndex() {
    //1.开始清空岗位solr索引
    SolrDataQuery query= new SimpleQuery("*:*");
    solrService.deleteByQuery("JOB", query);
    //2.查询数据库中的职位数据
    List<Job> jobList = jobService.queryJobList();
    //3.将数据库中查询出来的gpsx和gpsy构建出location字段。格式:"lat,lon",即"gpsy,gpsx"
    if(jobList != null && jobList.size() > 0){
        List<VJobSorlDataDto> list = DTOUtil.copyList(jobList, VJobSorlDataDto.class)
                        .stream()
                        .peek(index -> index.setLocation(index.getGpsy() + "," + index.getGpsx())) // 组合 gpsx 和 gpsy
                        .collect(Collectors.toList());
    }
    //4.构建solr索引数据
    solrService.uploadBeans("JOB", list);

}

构建完成之后可以看到Solr中已经有数据了(省略了业务字段)

{
  "responseHeader":{
    "status":0,
    "QTime":1,
    "params":{
      "q":"*:*",
      "_":"1736996604400"}},
  "response":{"numFound":8,"start":0,"docs":[
    {
      "id":"4ce9da0868e1f4345ffdb9b9224a5339",
      "location":"27.283615,105.291544",
      "GPSX":105.291544,
      "GPSY":27.283615,
      "_version_":1821384629722021888},
    {
      "id":"32d64f3217fc882c15dfce3b2ceefb8a",
      "location":"0.0,0.0",     
      "GPSX":0.0,
      "GPSY":0.0, 
      "_version_":1821384629725167616}]
  }}

下面是一个简单的示例:

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.common.SolrInputDocument;

public class SolrSpatialExample {
    public static void main(String[] args) throws Exception {
        String urlString = "http://localhost:8983/solr/mycore";
        SolrClient solr = new HttpSolrClient.Builder(urlString).build();

        SolrInputDocument doc = new SolrInputDocument();
        doc.addField("id", "1");
        doc.addField("name", "Example Location");
        doc.addField("location", "40.7128,-74.0060"); // New York City coordinates

        solr.add(doc);
        solr.commit();
    }
}

  1. 查询数据
@Override
@Transactional
public QueryResult<VCb21IndexDto> queryJobIndexPage(VCb21IndexDto dto, Integer pageNo, Integer pageSize) {
    StringBuilder querysql = new StringBuilder();

    if(StringUtils.isNotBlank(dto.getAce751())) {
        querysql.append(" +ACE751:").append(dto.getAce751());
    }

    SimpleQuery query = new SimpleQuery(querysql.toString());

    if (dto.getGpsx() != null && dto.getGpsy() != null && dto.getRange() != null) {
        String geofiltQuery = "{!geofilt sfield=location pt=" + dto.getGpsy() + "," + dto.getGpsx() + " d=" + Integer.valueOf(dto.getRange()) + "}";
        FilterQuery filterQuery = new SimpleFilterQuery(new Criteria(geofiltQuery));
        query.addFilterQuery(filterQuery);
    }

    List<Sort.Order> orders=new ArrayList<Sort.Order>();

    Sort.Order orderId = new Sort.Order(Sort.Direction.DESC,"id");
    orders.add(orderId);
    Sort sort = new Sort(orders);
    query.addSort(sort);

    query.setOffset((long) ((pageNo - 1) * pageSize));
    query.setRows(pageSize);
    System.out.println("=======================开始查询岗位solr索引,querysql:"+querysql+"=======================");
    Page<VJobSorlDataDto> indexpage = solrService.query(IndexConstDto.INDEX_JOB_COLLECTION, query, VJobSorlDataDto.class);
    QueryResult<VCb21IndexDto> result= SolrUtil.copyPage(indexpage.getContent(),indexpage.getTotalElements() ,pageNo, pageSize,VCb21IndexDto.class);
    System.out.println("=======================结束岗位solr索引,共查询到:"+indexpage.getTotalElements()+"条结果===================");
    return result;
}

大功告成。

下面是一个简单的示例:

在查询数据时,可以使用 Solr 提供的空间查询语法。以下是一个示例,展示了如何使用 SolrJ 库执行 Distance 查询,查找距离指定点 10 公里范围内的文档:

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocumentList;

public class SolrSpatialQueryExample {
    public static void main(String[] args) throws Exception {
        String urlString = "http://localhost:8983/solr/mycore";
        SolrClient solr = new HttpSolrClient.Builder(urlString).build();

        SolrQuery query = new SolrQuery();
        query.set("q", "{!geofilt sfield=location pt=40.7128,-74.0060 d=10}");

        QueryResponse response = solr.query(query);
        SolrDocumentList documents = response.getResults();

        for (int i = 0; i < documents.size(); ++i) {
            System.out.println(documents.get(i));
        }
    }
}

6. 应用场景

  1. 基于位置的服务(LBS):如外卖配送中,查找距离用户一定范围内的餐厅;打车应用中,定位附近的可用车辆等。
  2. 地理信息系统(GIS):在地图应用中,查询某个区域内的特定地理要素,如城市中的公园、河流等。
  3. 物流与运输:实时跟踪货物运输车辆的位置,优化配送路线,查询某个区域内的物流网点。
  4. 房地产行业:根据用户所在位置,推荐周边一定范围内的待售房产。