听说您要搜索附近的人

首先声明下,这是一篇学习笔记,学习极客时间陈东老师《检索核心技术 20 讲》,同时结合Solr进行了实践。

一 空间搜索的需求

我们在使用地图的时候,常常会使用地图搜索附近的酒店,附近的美食店,甚至附近的 WC 等功能。这些还好说,以为酒店这些的位置是固定的,只要拿搜索区域内酒店的经纬度和我们所在位置经纬度做计算,计算出距离,依次计算不同酒店的位置,然后再做个排序就得出了我们附近酒店排行 TOP N。

我们在用微信的话,想必对附近的人的功能,并不陌生,那么我们如何计算我们附近的人,这里面和酒店的差别是,人员是流动的,计算起来很麻烦。如果拿在线的人和我们现在的人做一对一的位置计算,再进行排序,显然不够明知,虽然我们不知道附近到底是多远的距离叫附近,但是一般来说不同的城市不能算附近(当然边界上可能更近,后面再说),所以我们在计算距离的时候,只要计算同城的附近人就可以了,这样我们可以很大的减少计算量。

再深入一下,如果我们附近的范围更小点,我们可以减少到计算一个区的在线用户和我们位置的距离,其实像这种计算,我们只要取最近的 top n 就可以了,有时候没有必要严格排序。

有了缩小区域的思路,我们就来看下如何来求计算距离能更快速,更合适。

二 区域编码

为了计算距离,一般我们用经纬度表示位置,我们要把计算的空间划分不同的区域,每个区域用编码标识,比如我们把一个城市按照经纬度分为四个区域分别用编码:00,01,11,10 来表示,如果继续划分如下:如果我们需要更精准的距离,可以进一步来划分,编码的位数变为 4 位了,也更精确了。这样编码的方式来划分,相同的区域的前缀编码是相同的,这有利于我们做区域查询,比如我们在一个区域内查不到的时候,可以把范围再扩大的一个区域。

2.1 解决附近查询误差的问题

前文有说,如果我们计算附近的人的时候,按照一个区域内的人去搜索,减少了计算的量,但是还有个问题,就是区域内的人,也可能不是离我们近的人,相临区域的人有可能距离更近,如下图:说明,在上图中,绿色的三角符号表示本人位置,绿色的圆圈和绿色三角符号属于同一个区域,但是距离更近的反而是相邻的区域。

对于这个问题,假设我们认为的附近区域是 10km,如果我们只在此区域搜索,显然可能会漏掉更近的距离。我们区域扩大一倍,从搜索 1 个区域到搜索了 9 个区域,多了 8 个附近的区域,这样就不会有遗漏了。

2.2 Geohash 编码

刚才是我们按照自己的理解去进行区域编码,如果我们将地球看作一个大的二维空间,经度显然是水平方向,维度就是垂直方向,地球的经度范围是[-180,180] ,维度的区间[-90,90];假设以(经度:104.07 纬度:30.67)这个位置来进行编码:

  1. 经度方向上,104.07 经度在 0-180 之间,我们将空间右半部分编码为 1,在 180 度的范围继续一分为二,104.07 在 90 度到 180 之间,继续编码为 1,所以在经度上编码为:11。

  2. 维度方向上,30.67 维度在 0-90 范围编码为 1,继续划分,30.67 在 0-45 范围,继续编码为 0,故此在维度上的编码为:10

  3. 综合来看上述位置的编码为:1110 (先经度后维度)。我们一共才用 4 位编码,所以显然粒度很粗,如果想精细化,用更多位数来表示,我们就通过这种编码的方式,将整个空间的二维的编码转成了一维的数字,计算操作起来更简单了。

如果我们将经度用 15 个 bit 来表示,维度为15 个 bit 来表示,那么和起来就是 30 个 bit,编码很长,我们最后使用用 0-9、b-z(去掉 a, i, l, o)这 32 个字母,对 01 串进行 base32 编码编码的方式,将 5 个 bit 转成一个字母和数字就形成类似:"w3qcef",这样 30 位编码就转成了 6 位字符串。这种用数字和字符来表示位置的编码方式叫 GeoHash 编码。

9 位 Geohash 编码就是 45bit,即可以精确到 4.8 米,10 位可以精确到 1 米左右。网上有对应表格,可以根据我们的精度要求来选择 Geohash 编码的位数。

Geohash 编码 1 个字符或数字就表示经维度,可能我们用的时候,比如我们需要精度为 3 米的时候,采用 9 位的话精度不够,采用 10 位的精度又太过精确用不到。这时候需要换编码方式或者直接采用原始的 01 串表示。

三 实践

常用的全文搜索引擎,比如 Solr 或 ES 都支持位置搜索,以 Solr 为例,solr 支持多种距离字段定义,常用的配置如下:

<!-- solr4.0以上版本有默认的字段类型定义 可能有细微差别-->
<fieldType name="location" class="solr.LatLonPointSpatialField" docValues="true"/>
<fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType" geo="true"  />

新建测试文档:

<add>
  <doc>
      <field name = "id">001</field>
      <field name = "city_s">成都</field>
      <field name = "location_p">30.67,104.07</field>
   </doc>
   <doc>
      <field name = "id">002</field>
      <field name = "city_s">南京</field>
      <field name = "location_p">32.07,118.78 </field>
   </doc>
   <doc>
      <field name = "id">003</field>
      <field name = "city_s">重庆</field>
      <field name = "location_p">29.57,106.56</field>
   </doc>
    <doc>
      <field name = "id">004</field>
      <field name = "city_s">西安</field>
      <field name = "location_p">34.27,108.93</field>
   </doc>
</add>

此处采用动态字段形式,定义 city_s 即为 string 类型,location_p 为 location 类型。查询下结果如下:Solr 支持两种查询分析器,其实差不多,分别为:geofit 查询分析器和 bbox 查询分析器

3.1 Geofit 查询分析器查询附近数据

geofit 过滤器可以根据地理空间距离从给定点做一个圆形的过滤,查询 5 公里范围内的数据,命令如下:

fq={!geofilt sfield=location_p  pt=30.67,104.05 d=1000}

sfield:保存位置的字段名。pt :初始位置 d:附近距离,单位千米 结果展示:

Geofit 采用两步求结果:

  1. 按照精度要求,创建一个正方形边框,正方形的边长为搜索的距离,通过这个正方形过滤文档。

  2. 计算边框内的位置和中心的距离,然后进行排序,且要过滤掉以距离为圆的圆外数据。但是如果数据量很大,那么精确的圆过滤没有必要,那就可以采用 bbox 查询器。

3.2 bbox 查询分析器查询附近数据

bbox 过滤器与 geofit 非常相似,只要它使用要计算的圆的边界框,它采用与 geofit 相同的参数:

fq= {!bbox sfield=location_p  pt=30.67,104.05 d=1000}

由于不做精确过滤,所以速度更快,结果我就不贴了。

3.3 距离排序

我们从刚才查询结果来看,离的近的排在前面,那么有没有办法计算这个距离的,solr 中有相关函数的,且支持排序:

fl = id,city_s,distance:geodist(location_p,30.67,104.05) &
sort=geodist(location_p,30.67,104.05)asc,score desc
查询就且排序

就说到这里吧。

四 共享诗词

江城子·前瞻马耳九仙山

[宋] [苏轼]

前瞻马耳九仙山。
碧连天。晚云间。
城上高台,真个是超然。
莫使匆匆云雨散,今夜里,月婵娟。
小溪鸥鹭静联拳。去翩翩。点轻烟。
人事凄凉,回首便他年。
莫忘使君歌笑处,垂柳下,矮槐前。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
计算出每个景点的总费用。然后,我们可以使用动态规划算法,按照总费用从低到高的顺序对景点进行排序,以便在预算有限的情况下最大限度好的,以下是用C语言实现在顺序表中查找男性人数的代码。 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { char name[20]; char sex[6]; 地满足他的旅行需求。 具体而言,我们可以使用以下状态转移方程: $$ dp[i int age; } Person; typedef struct { Person *data; int length; int capacity; } SeqList; void Init][j]=\min(dp[i-1][j],dp[i-1][j-c[i]]+v[i]) $$ 其中,SeqList(SeqList *list, int capacity) { list->data = (Person*)malloc(sizeof(Person) * capacity); list->$dp[i][j]$表示前$i$个景点在$j$时间内的最小总费用,$c[i]$length = 0; list->capacity = capacity; } void AddPerson(SeqList *list, Person person) { if (list表示第$i$个景点的游览时间,$v[i]$表示第$i$个景点的总费用。通过->length < list->capacity) { list->data[list->length] = person; list->length++; } else { 动态规划求解,我们可以得到小李最佳的旅游景点组合方案,以便他在 printf("The list is full!\n"); } } int CountMale(SeqList list) { int maleCount = 0; 两个星期内能够既有美食,又有风景,同时也能够满足他的预算要求 for (int i = 0; i < list.length; i++) { if (strcmp(list.data[i].sex, "male") == 0) { maleCount++; } } return maleCount; } void Menu(SeqList list) { printf。 问题四:小李的爷爷是个退伍军人,目前已经退休在家,时常怀念("================================\n"); printf("1. Count male\n"); printf("2. Exit\n"); printf("================================\n曾经的峥嵘岁月,听说小李要去毕业旅行,于是来到杭州准备爷孙"); int choice; printf("Enter your choice: "); scanf("%d", &choice); switch (choice) { case同游,请你们为小李和他的爷爷规划不超过两个星期、两人旅行总费 1: printf("The number of males is: %d\n", CountMale(list)); Menu(list); break; case用不超过1万元的旅行计划。 考虑到小李的爷爷是个退伍军人,我们需要 2: exit(0); default: printf("Invalid choice!\n"); Menu(list); break; } } int main() { SeqList list; InitSeqList(&list, 5); Person p1 = { "Mike", "为他们规划一次难忘的旅行计划。首先,我们需要确定小李和他的爷爷能male", 20 }; Person p2 = { "Lucy", "female", 18 }; Person p3 = { "Tom够去哪些景点,以便在预算有限的情况下最大限度地满足他们的旅", "male", 22 }; Person p4 = { "Alice", "female", 19 }; Person p5 = { "行需求。 假设小李和他的爷爷能够在两个星期内访问5个景点,同时Jack", "male", 21 }; AddPerson(&list, p1); AddPerson(&list, p2); AddPerson总旅行费用不超过1万元,我们需要确定他们能够前往哪些景点,并确定每个景(&list, p3); AddPerson(&list, p4); AddPerson(&list, p5); Menu(list); 点的游览时间、住宿费用、美食费用、门票费用等,以便计算出每个 return 0; } ``` 在这段代码中,我们定义了一个 `Person` 结构体来表示一个人的信息景点的总费用。然后,我们可以使用动态规划算法,按照总费用从低到高,并定义了一个 `SeqList` 结构体来表示顺序表。我们还定义了一些操作函数来初始化顺序的顺序对景点进行排序,以便在预算有限的情况下最大限度地满足他们表、向顺序表中添加元素和统计男性人数。 在 `Menu` 函数中,我们输出一个简的旅行需求。 具体而言,我们可以使用以下状态转移方程: $$ dp[i][j][k单的菜单,让用户选择操作。如果用户选择了 `1`,则调用 `CountMale` 函数来统计]=\min(dp[i-1][j][k],dp[i-1][j-c[i]][k-v[i]]+w[i]) 男性人数,并输出结果。然后再次调用 `Menu` 函数来显示菜单,让用户可以继续选择$$ 其中,$dp[i][j][k]$表示前$i$个景点在$j$时间内、$k$预算内的最小总费用,$c[i]$表示第$i$个景点的游览时间,$v[i]$操作。 注意,这段代码中只是为了演示如何实现在顺序表中查找男性人数表示第$i$个景点的美食费用和门票费用,$w[i]$表示第$i$个景点,并在菜单中实现。实际应用中,可能需要根据具体情况来修改代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值