使用Lucene-Spatial实现集成地理位置的全文检索

Lucene通过Spatial包提供了对基于地理位置的全文检索的支持,最典型的应用场景就是:“搜索中关村附近1公里内的火锅店,并按远近排序”。使用Lucene-Spatial添加对地理位置的支持,和之前普通文本搜索主要有两点区别:

        1. 将坐标信息转化为笛卡尔层,建立索引

  1. private void indexLocation(Document document, JSONObject jo)
  2. throws Exception {
  3. double longitude = jo.getDouble("longitude");
  4. double latitude = jo.getDouble("latitude");
  5. document.add(new Field("lat", NumericUtils
  6. .doubleToPrefixCoded(latitude), Field.Store.YES,
  7. Field.Index.NOT_ANALYZED));
  8. document.add(new Field("lng", NumericUtils
  9. .doubleToPrefixCoded(longitude), Field.Store.YES,
  10. Field.Index.NOT_ANALYZED));
  11. for (int tier = startTier; tier <= endTier; tier++) {
  12. ctp = new CartesianTierPlotter(tier, projector,
  13. CartesianTierPlotter.DEFALT_FIELD_PREFIX);
  14. final double boxId = ctp.getTierBoxId(latitude, longitude);
  15. document.add(new Field(ctp.getTierFieldName(), NumericUtils
  16. .doubleToPrefixCoded(boxId), Field.Store.YES,
  17. Field.Index.NOT_ANALYZED_NO_NORMS));
  18. }
  19. }
      private void indexLocation(Document document, JSONObject jo)
throws Exception { double longitude = jo.getDouble("longitude");
double latitude = jo.getDouble("latitude"); document.add(new Field("lat", NumericUtils
.doubleToPrefixCoded(latitude), Field.Store.YES,
Field.Index.NOT_ANALYZED));
document.add(new Field("lng", NumericUtils
.doubleToPrefixCoded(longitude), Field.Store.YES,
Field.Index.NOT_ANALYZED)); for (int tier = startTier; tier <= endTier; tier++) {
ctp = new CartesianTierPlotter(tier, projector,
CartesianTierPlotter.DEFALT_FIELD_PREFIX);
final double boxId = ctp.getTierBoxId(latitude, longitude);
document.add(new Field(ctp.getTierFieldName(), NumericUtils
.doubleToPrefixCoded(boxId), Field.Store.YES,
Field.Index.NOT_ANALYZED_NO_NORMS));
}
}

2. 搜索时,指定使用DistanceQueryFilter

  1. DistanceQueryBuilder dq = new DistanceQueryBuilder(latitude,
  2. longitude, miles, "lat", "lng",
  3. CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, startTier,
  4. endTier);
  5. DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(
  6. dq.getDistanceFilter());
  7. Sort sort = new Sort(new SortField("geo_distance", dsort));
DistanceQueryBuilder dq = new DistanceQueryBuilder(latitude,
longitude, miles, "lat", "lng",
CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, startTier,
endTier);
DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(
dq.getDistanceFilter());
Sort sort = new Sort(new SortField("geo_distance", dsort));

下面是基于Lucene3.2.0和JUnit4.8.2的完整代码。

  1. <dependencies>
  2. <dependency>
  3. <groupId>junit</groupId>
  4. <artifactId>junit</artifactId>
  5. <version>4.8.2</version>
  6. <type>jar</type>
  7. <scope>test</scope>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.apache.lucene</groupId>
  11. <artifactId>lucene-core</artifactId>
  12. <version>3.2.0</version>
  13. <type>jar</type>
  14. <scope>compile</scope>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.apache.lucene</groupId>
  18. <artifactId>lucene-spatial</artifactId>
  19. <version>3.2.0</version>
  20. <type>jar</type>
  21. <scope>compile</scope>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.json</groupId>
  25. <artifactId>json</artifactId>
  26. <version>20100903</version>
  27. <type>jar</type>
  28. <scope>compile</scope>
  29. </dependency>
  30. </dependencies>
  <dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<type>jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>3.2.0</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-spatial</artifactId>
<version>3.2.0</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20100903</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
</dependencies>

首先准备测试用的数据:

  1. {"id":12,"title":"时尚码头美容美发热烫特价","longitude":116.3838183,"latitude":39.9629015}
  2. {"id":17,"title":"审美个人美容美发套餐","longitude":116.386564,"latitude":39.966102}
  3. {"id":23,"title":"海底捞吃300送300","longitude":116.38629,"latitude":39.9629573}
  4. {"id":26,"title":"仅98元!享原价335元李老爹","longitude":116.3846175,"latitude":39.9629125}
  5. {"id":29,"title":"都美造型烫染美发护理套餐","longitude":116.38629,"latitude":39.9629573}
  6. {"id":30,"title":"仅售55元!原价80元的老舍茶馆相声下午场","longitude":116.0799914,"latitude":39.9655391}
  7. {"id":33,"title":"仅售55元!原价80元的新笑声客栈早场","longitude":116.0799914,"latitude":39.9655391}
  8. {"id":34,"title":"仅售39元(红色礼盒)!原价80元的平谷桃","longitude":116.0799914,"latitude":39.9655391}
  9. {"id":46,"title":"仅售38元!原价180元地质礼堂白雪公主","longitude":116.0799914,"latitude":39.9655391}
  10. {"id":49,"title":"仅99元!享原价342.7元自助餐","longitude":116.0799914,"latitude":39.9655391}
  11. {"id":58,"title":"桑海教育暑期学生报名培训九折优惠券","longitude":116.0799914,"latitude":39.9655391}
  12. {"id":59,"title":"全国发货:仅29元!贝玲妃超模粉红高光光","longitude":116.0799914,"latitude":39.9655391}
  13. {"id":65,"title":"海之屿生态水族用品店抵用券","longitude":116.0799914,"latitude":39.9655391}
  14. {"id":67,"title":"小区东门时尚烫染个人护理美发套餐","longitude":116.3799914,"latitude":39.9655391}
  15. {"id":74,"title":"《郭德纲相声专辑》CD套装","longitude":116.0799914,"latitude":39.9655391}
{"id":12,"title":"时尚码头美容美发热烫特价","longitude":116.3838183,"latitude":39.9629015}
{"id":17,"title":"审美个人美容美发套餐","longitude":116.386564,"latitude":39.966102}
{"id":23,"title":"海底捞吃300送300","longitude":116.38629,"latitude":39.9629573}
{"id":26,"title":"仅98元!享原价335元李老爹","longitude":116.3846175,"latitude":39.9629125}
{"id":29,"title":"都美造型烫染美发护理套餐","longitude":116.38629,"latitude":39.9629573}
{"id":30,"title":"仅售55元!原价80元的老舍茶馆相声下午场","longitude":116.0799914,"latitude":39.9655391}
{"id":33,"title":"仅售55元!原价80元的新笑声客栈早场","longitude":116.0799914,"latitude":39.9655391}
{"id":34,"title":"仅售39元(红色礼盒)!原价80元的平谷桃","longitude":116.0799914,"latitude":39.9655391}
{"id":46,"title":"仅售38元!原价180元地质礼堂白雪公主","longitude":116.0799914,"latitude":39.9655391}
{"id":49,"title":"仅99元!享原价342.7元自助餐","longitude":116.0799914,"latitude":39.9655391}
{"id":58,"title":"桑海教育暑期学生报名培训九折优惠券","longitude":116.0799914,"latitude":39.9655391}
{"id":59,"title":"全国发货:仅29元!贝玲妃超模粉红高光光","longitude":116.0799914,"latitude":39.9655391}
{"id":65,"title":"海之屿生态水族用品店抵用券","longitude":116.0799914,"latitude":39.9655391}
{"id":67,"title":"小区东门时尚烫染个人护理美发套餐","longitude":116.3799914,"latitude":39.9655391}
{"id":74,"title":"《郭德纲相声专辑》CD套装","longitude":116.0799914,"latitude":39.9655391}

根据上面的测试数据,编写测试用例,分别搜索坐标(116.3838183, 39.96290153千米以内的“美发”和全部内容,分别得到的结果应该是4条和6条。

  1. import static org.junit.Assert.assertEquals;
  2. import static org.junit.Assert.fail;
  3. import java.util.List;
  4. import org.junit.Test;
  5. public class LuceneSpatialTest {
  6. private static LuceneSpatial spatialSearcher = new LuceneSpatial();
  7. @Test
  8. public void testSearch() {
  9. try {
  10. long start = System.currentTimeMillis();
  11. List<String> results = spatialSearcher.search("美发", 116.3838183, 39.9629015, 3.0);
  12. System.out.println(results.size()
  13. + "个匹配结果,共耗时 "
  14. + (System.currentTimeMillis() - start) + "毫秒。\n");
  15. assertEquals(4, results.size());
  16. } catch (Exception e) {
  17. fail("Exception occurs...");
  18. e.printStackTrace();
  19. }
  20. }
  21. @Test
  22. public void testSearchWithoutKeyword() {
  23. try {
  24. long start = System.currentTimeMillis();
  25. List<String> results = spatialSearcher.search(null, 116.3838183, 39.9629015, 3.0);
  26. System.out.println( results.size()
  27. + "个匹配结果,共耗时 "
  28. + (System.currentTimeMillis() - start) + "毫秒.\n");
  29. assertEquals(6, results.size());
  30. } catch (Exception e) {
  31. fail("Exception occurs...");
  32. e.printStackTrace();
  33. }
  34. }
  35. }
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import java.util.List; import org.junit.Test; public class LuceneSpatialTest { private static LuceneSpatial spatialSearcher = new LuceneSpatial(); @Test
public void testSearch() {
try {
long start = System.currentTimeMillis();
List<String> results = spatialSearcher.search("美发", 116.3838183, 39.9629015, 3.0);
System.out.println(results.size()
+ "个匹配结果,共耗时 "
+ (System.currentTimeMillis() - start) + "毫秒。\n");
assertEquals(4, results.size());
} catch (Exception e) {
fail("Exception occurs...");
e.printStackTrace();
}
} @Test
public void testSearchWithoutKeyword() {
try {
long start = System.currentTimeMillis();
List<String> results = spatialSearcher.search(null, 116.3838183, 39.9629015, 3.0);
System.out.println( results.size()
+ "个匹配结果,共耗时 "
+ (System.currentTimeMillis() - start) + "毫秒.\n");
assertEquals(6, results.size());
} catch (Exception e) {
fail("Exception occurs...");
e.printStackTrace();
}
}
}

下面是LuceneSpatial类,在构造函数中初始化变量和创建索引:

  1. public class LuceneSpatial {
  2. private Analyzer analyzer;
  3. private IndexWriter writer;
  4. private FSDirectory indexDirectory;
  5. private IndexSearcher indexSearcher;
  6. private IndexReader indexReader;
  7. private String indexPath = "c:/lucene-spatial";
  8. // Spatial
  9. private IProjector projector;
  10. private CartesianTierPlotter ctp;
  11. public static final double RATE_MILE_TO_KM = 1.609344; //英里和公里的比率
  12. public static final String LAT_FIELD = "lat";
  13. public static final String LON_FIELD = "lng";
  14. private static final double MAX_RANGE = 15.0; // 索引支持的最大范围,单位是千米
  15. private static final double MIN_RANGE = 3.0;  // 索引支持的最小范围,单位是千米
  16. private int startTier;
  17. private int endTier;
  18. public LuceneSpatial() {
  19. try {
  20. init();
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. private void init() throws Exception {
  26. initializeSpatialOptions();
  27. analyzer = new StandardAnalyzer(Version.LUCENE_32);
  28. File path = new File(indexPath);
  29. boolean isNeedCreateIndex = false;
  30. if (path.exists() && !path.isDirectory())
  31. throw new Exception("Specified path is not a directory");
  32. if (!path.exists()) {
  33. path.mkdirs();
  34. isNeedCreateIndex = true;
  35. }
  36. indexDirectory = FSDirectory.open(new File(indexPath));
  37. //建立索引
  38. if (isNeedCreateIndex) {
  39. IndexWriterConfig indexWriterConfig = new IndexWriterConfig(
  40. Version.LUCENE_32, analyzer);
  41. indexWriterConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
  42. writer = new IndexWriter(indexDirectory, indexWriterConfig);
  43. buildIndex();
  44. }
  45. indexReader = IndexReader.open(indexDirectory, true);
  46. indexSearcher = new IndexSearcher(indexReader);
  47. }
  48. @SuppressWarnings("deprecation")
  49. private void initializeSpatialOptions() {
  50. projector = new SinusoidalProjector();
  51. ctp = new CartesianTierPlotter(0, projector,
  52. CartesianTierPlotter.DEFALT_FIELD_PREFIX);
  53. startTier = ctp.bestFit(MAX_RANGE / RATE_MILE_TO_KM);
  54. endTier = ctp.bestFit(MIN_RANGE / RATE_MILE_TO_KM);
  55. }
  56. private int mile2Meter(double miles) {
  57. double dMeter = miles * RATE_MILE_TO_KM * 1000;
  58. return (int) dMeter;
  59. }
  60. private double km2Mile(double km) {
  61. return km / RATE_MILE_TO_KM;
  62. }
public class LuceneSpatial {

	private Analyzer analyzer;
private IndexWriter writer;
private FSDirectory indexDirectory;
private IndexSearcher indexSearcher;
private IndexReader indexReader;
private String indexPath = "c:/lucene-spatial"; // Spatial
private IProjector projector;
private CartesianTierPlotter ctp;
public static final double RATE_MILE_TO_KM = 1.609344; //英里和公里的比率
public static final String LAT_FIELD = "lat";
public static final String LON_FIELD = "lng";
private static final double MAX_RANGE = 15.0; // 索引支持的最大范围,单位是千米
private static final double MIN_RANGE = 3.0; // 索引支持的最小范围,单位是千米
private int startTier;
private int endTier; public LuceneSpatial() {
try {
init();
} catch (Exception e) {
e.printStackTrace();
}
} private void init() throws Exception {
initializeSpatialOptions(); analyzer = new StandardAnalyzer(Version.LUCENE_32); File path = new File(indexPath); boolean isNeedCreateIndex = false; if (path.exists() && !path.isDirectory())
throw new Exception("Specified path is not a directory"); if (!path.exists()) {
path.mkdirs();
isNeedCreateIndex = true;
} indexDirectory = FSDirectory.open(new File(indexPath)); //建立索引
if (isNeedCreateIndex) {
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(
Version.LUCENE_32, analyzer);
indexWriterConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
writer = new IndexWriter(indexDirectory, indexWriterConfig);
buildIndex();
} indexReader = IndexReader.open(indexDirectory, true);
indexSearcher = new IndexSearcher(indexReader); } @SuppressWarnings("deprecation")
private void initializeSpatialOptions() {
projector = new SinusoidalProjector();
ctp = new CartesianTierPlotter(0, projector,
CartesianTierPlotter.DEFALT_FIELD_PREFIX);
startTier = ctp.bestFit(MAX_RANGE / RATE_MILE_TO_KM);
endTier = ctp.bestFit(MIN_RANGE / RATE_MILE_TO_KM);
} private int mile2Meter(double miles) {
double dMeter = miles * RATE_MILE_TO_KM * 1000; return (int) dMeter;
} private double km2Mile(double km) {
return km / RATE_MILE_TO_KM;
}

创建索引的具体实现:

  1. private void buildIndex() {
  2. BufferedReader br = null;
  3. try {
  4. //逐行添加测试数据到索引中,测试数据文件和源文件在同一个目录下
  5. br = new BufferedReader(new InputStreamReader(
  6. LuceneSpatial.class.getResourceAsStream("data")));
  7. String line = null;
  8. while ((line = br.readLine()) != null) {
  9. index(new JSONObject(line));
  10. }
  11. writer.commit();
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. } finally {
  15. if (br != null) {
  16. try {
  17. br.close();
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }
  23. }
  24. private void index(JSONObject jo) throws Exception {
  25. Document doc = new Document();
  26. doc.add(new Field("id", jo.getString("id"), Field.Store.YES,
  27. Field.Index.ANALYZED));
  28. doc.add(new Field("title", jo.getString("title"), Field.Store.YES,
  29. Field.Index.ANALYZED));
  30. //将位置信息添加到索引中
  31. indexLocation(doc, jo);
  32. writer.addDocument(doc);
  33. }
  34. private void indexLocation(Document document, JSONObject jo)
  35. throws Exception {
  36. double longitude = jo.getDouble("longitude");
  37. double latitude = jo.getDouble("latitude");
  38. document.add(new Field("lat", NumericUtils
  39. .doubleToPrefixCoded(latitude), Field.Store.YES,
  40. Field.Index.NOT_ANALYZED));
  41. document.add(new Field("lng", NumericUtils
  42. .doubleToPrefixCoded(longitude), Field.Store.YES,
  43. Field.Index.NOT_ANALYZED));
  44. for (int tier = startTier; tier <= endTier; tier++) {
  45. ctp = new CartesianTierPlotter(tier, projector,
  46. CartesianTierPlotter.DEFALT_FIELD_PREFIX);
  47. final double boxId = ctp.getTierBoxId(latitude, longitude);
  48. document.add(new Field(ctp.getTierFieldName(), NumericUtils
  49. .doubleToPrefixCoded(boxId), Field.Store.YES,
  50. Field.Index.NOT_ANALYZED_NO_NORMS));
  51. }
  52. }
	private void buildIndex() {
BufferedReader br = null;
try {
//逐行添加测试数据到索引中,测试数据文件和源文件在同一个目录下
br = new BufferedReader(new InputStreamReader(
LuceneSpatial.class.getResourceAsStream("data")));
String line = null;
while ((line = br.readLine()) != null) {
index(new JSONObject(line));
} writer.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} private void index(JSONObject jo) throws Exception {
Document doc = new Document(); doc.add(new Field("id", jo.getString("id"), Field.Store.YES,
Field.Index.ANALYZED)); doc.add(new Field("title", jo.getString("title"), Field.Store.YES,
Field.Index.ANALYZED)); //将位置信息添加到索引中
indexLocation(doc, jo); writer.addDocument(doc);
} private void indexLocation(Document document, JSONObject jo)
throws Exception { double longitude = jo.getDouble("longitude");
double latitude = jo.getDouble("latitude"); document.add(new Field("lat", NumericUtils
.doubleToPrefixCoded(latitude), Field.Store.YES,
Field.Index.NOT_ANALYZED));
document.add(new Field("lng", NumericUtils
.doubleToPrefixCoded(longitude), Field.Store.YES,
Field.Index.NOT_ANALYZED)); for (int tier = startTier; tier <= endTier; tier++) {
ctp = new CartesianTierPlotter(tier, projector,
CartesianTierPlotter.DEFALT_FIELD_PREFIX);
final double boxId = ctp.getTierBoxId(latitude, longitude);
document.add(new Field(ctp.getTierFieldName(), NumericUtils
.doubleToPrefixCoded(boxId), Field.Store.YES,
Field.Index.NOT_ANALYZED_NO_NORMS));
}
}

搜索的具体实现:

  1. public List<String> search(String keyword, double longitude,
  2. double latitude, double range) throws Exception {
  3. List<String> result = new ArrayList<String>();
  4. double miles = km2Mile(range);
  5. DistanceQueryBuilder dq = new DistanceQueryBuilder(latitude,
  6. longitude, miles, "lat", "lng",
  7. CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, startTier,
  8. endTier);
  9. //按照距离排序
  10. DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(
  11. dq.getDistanceFilter());
  12. Sort sort = new Sort(new SortField("geo_distance", dsort));
  13. Query query = buildQuery(keyword);
  14. //搜索结果
  15. TopDocs hits = indexSearcher.search(query, dq.getFilter(),
  16. Integer.MAX_VALUE, sort);
  17. //获得各条结果相对应的距离
  18. Map<Integer, Double> distances = dq.getDistanceFilter()
  19. .getDistances();
  20. for (int i = 0; i < hits.totalHits; i++) {
  21. final int docID = hits.scoreDocs[i].doc;
  22. final Document doc = indexSearcher.doc(docID);
  23. final StringBuilder builder = new StringBuilder();
  24. builder.append("找到了: ")
  25. .append(doc.get("title"))
  26. .append(", 距离: ")
  27. .append(mile2Meter(distances.get(docID)))
  28. .append("米。");
  29. System.out.println(builder.toString());
  30. result.add(builder.toString());
  31. }
  32. return result;
  33. }
  34. private Query buildQuery(String keyword) throws Exception {
  35. //如果没有指定关键字,则返回范围内的所有结果
  36. if (keyword == null || keyword.isEmpty()) {
  37. return new MatchAllDocsQuery();
  38. }
  39. QueryParser parser = new QueryParser(Version.LUCENE_32, "title",
  40. analyzer);
  41. parser.setDefaultOperator(Operator.AND);
  42. return parser.parse(keyword.toString());
  43. }
	public List<String> search(String keyword, double longitude,
double latitude, double range) throws Exception {
List<String> result = new ArrayList<String>(); double miles = km2Mile(range); DistanceQueryBuilder dq = new DistanceQueryBuilder(latitude,
longitude, miles, "lat", "lng",
CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, startTier,
endTier); //按照距离排序
DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(
dq.getDistanceFilter());
Sort sort = new Sort(new SortField("geo_distance", dsort)); Query query = buildQuery(keyword); //搜索结果
TopDocs hits = indexSearcher.search(query, dq.getFilter(),
Integer.MAX_VALUE, sort);
//获得各条结果相对应的距离
Map<Integer, Double> distances = dq.getDistanceFilter()
.getDistances(); for (int i = 0; i < hits.totalHits; i++) {
final int docID = hits.scoreDocs[i].doc; final Document doc = indexSearcher.doc(docID); final StringBuilder builder = new StringBuilder();
builder.append("找到了: ")
.append(doc.get("title"))
.append(", 距离: ")
.append(mile2Meter(distances.get(docID)))
.append("米。");
System.out.println(builder.toString()); result.add(builder.toString());
} return result;
} private Query buildQuery(String keyword) throws Exception {
//如果没有指定关键字,则返回范围内的所有结果
if (keyword == null || keyword.isEmpty()) {
return new MatchAllDocsQuery();
}
QueryParser parser = new QueryParser(Version.LUCENE_32, "title",
analyzer); parser.setDefaultOperator(Operator.AND); return parser.parse(keyword.toString());
}

执行测试用例,可以得到下面的结果:

  1. 找到了: 时尚码头美容美发热烫特价, 距离: 0米。
  2. 找到了: 都美造型烫染美发护理套餐, 距离: 210米。
  3. 找到了: 审美个人美容美发套餐, 距离: 426米。
  4. 找到了: 小区东门时尚烫染个人护理美发套餐, 距离: 439米。
  5. 4个匹配结果,共耗时 119毫秒。
  6. 找到了: 时尚码头美容美发热烫特价, 距离: 0米。
  7. 找到了: 仅98元!享原价335元李老爹, 距离: 68米。
  8. 找到了: 海底捞吃300送300, 距离: 210米。
  9. 找到了: 都美造型烫染美发护理套餐, 距离: 210米。
  10. 找到了: 审美个人美容美发套餐, 距离: 426米。
  11. 找到了: 小区东门时尚烫染个人护理美发套餐, 距离: 439米。
  12. 6个匹配结果,共耗时 3毫秒.
找到了: 时尚码头美容美发热烫特价, 距离: 0米。
找到了: 都美造型烫染美发护理套餐, 距离: 210米。
找到了: 审美个人美容美发套餐, 距离: 426米。
找到了: 小区东门时尚烫染个人护理美发套餐, 距离: 439米。
4个匹配结果,共耗时 119毫秒。 找到了: 时尚码头美容美发热烫特价, 距离: 0米。
找到了: 仅98元!享原价335元李老爹, 距离: 68米。
找到了: 海底捞吃300送300, 距离: 210米。
找到了: 都美造型烫染美发护理套餐, 距离: 210米。
找到了: 审美个人美容美发套餐, 距离: 426米。
找到了: 小区东门时尚烫染个人护理美发套餐, 距离: 439米。
6个匹配结果,共耗时 3毫秒.

参考文献:

Lucene-Spatial的原理介绍:http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene.htm

GeoHash:http://en.wikipedia.org/wiki/Geohash

两篇示例(其中大部分代码就来自于这里):

Spatial search with Lucene

上一篇:IOS第六天(3:scrollView 图片轮播器)


下一篇:小白的Python之路 day5 logging模块