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

使用Lucene-Spatial实现集成地理位置的全文检索的更多相关文章

  1. Lucene Spatial构建地理空间索引

    一.Maven依赖 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="h ...

  2. jieba.NET与Lucene.Net的集成

    首先声明:我对Lucene.Net并不熟悉,但搜索确实是分词的一个重要应用,所以这里还是尝试将两者集成起来,也许对你有一参考. 看到了两个中文分词与Lucene.Net的集成项目:Lucene.Net ...

  3. 【转】jieba.NET与Lucene.Net的集成

    首先声明:我对Lucene.Net并不熟悉,但搜索确实是分词的一个重要应用,所以这里还是尝试将两者集成起来,也许对你有一参考. 看到了两个中文分词与Lucene.Net的集成项目:Lucene.Net ...

  4. ]NET Core Lucene.net和PanGu分词实现全文检索

    Lucene.net和PanGu分词实现全文检索 Lucene.net(4.8.0) 学习问题记录五: JIEba分词和Lucene的结合,以及对分词器的思考   前言:目前自己在做使用Lucene. ...

  5. Lucene学习笔记:一,全文检索的基本原理

    一.总论 根据http://lucene.apache.org/java/docs/index.html定义: Lucene是一个高效的,基于Java的全文检索库. 所以在了解Lucene之前要费一番 ...

  6. Lucene的配置及创建索引全文检索

    Lucene 是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言).Lucene ...

  7. Lucene系列二:Lucene(Lucene介绍、Lucene架构、Lucene集成)

    一.Lucene介绍 1. Lucene简介 最受欢迎的java开源全文搜索引擎开发工具包.提供了完整的查询引擎和索引引擎,部分文本分词引擎(英文与德文两种西方语言).Lucene的目的是为软件开发人 ...

  8. 全文检索Lucene (1)

    Lucene是apache开源的一个全文检索框架,很是出名.今天先来分享一个类似于HelloWorld级别的使用. 工作流程 依赖 我们要想使用Lucene,那就得先引用人家的jar包了.下面列举一下 ...

  9. Lucene 01 - 初步认识全文检索和Lucene

    目录 1 搜索简介 1.1 搜索实现方案 1.2 数据查询方法 1.2.1 顺序扫描法 1.2.2 倒排索引法(反向索引) 1.3 搜索技术应用场景 2 Lucene简介 2.1 Lucene是什么 ...

随机推荐

  1. sourcetree合并分支

    参考: https://blog.csdn.net/qq_34975710/article/details/74469068

  2. IDEA使用SpringBoot 、maven创建微服务的简单过程

    使用IDEA新建一个简单的微服务 1. 打开IDEA,File -> New  -> project 打开如下图1-1所示的对话框 图 1-1 2.点击"Next"按钮 ...

  3. Kubelet bootstrap认证配置步骤

    kubelet 授权 kube-apiserver 的一些操作 exec run logs 等 RBAC 只需创建一次就可以 kubectl create clusterrolebinding kub ...

  4. AR涂涂乐

    <1> 涂涂乐着色 https://blog.csdn.net/begonia__z/article/details/51282932 http://www.manew.com/blog- ...

  5. 原生js实现Base64编码解码

    注:ie10+ var str = window.btoa("liusong"); console.log(str); var s = window.atob("bGl1 ...

  6. ASP.NET Boilerplate-AbpSession

    /------2016-05-15/------介绍 如果一个应用支持登录,也许需要知道当前登录用户的一些操作,然而ASP.NET 本身对于展现层提供了Session的支持,ABP提供了 IAbpSe ...

  7. 服务器&linux

    linux vsftp查看端口占用:netstat -natp |grep 21如果有占用21端口进程,kill它 ,或者remove它.安装:yum -y install vsftpduseradd ...

  8. oracle 查询索引和主键

    ORACLE: 1.查主键名称: select * from user_constraints where table_name = 'AAA' and constraint_type ='P'; 查 ...

  9. 156. Binary Tree Upside Down反转二叉树

    [抄题]: Given a binary tree where all the right nodes are either leaf nodes with a sibling (a left nod ...

  10. 整理的最全 python常见面试题(基本必考)

    整理的最全 python常见面试题(基本必考) python 2018-05-17 作者 大蛇王 1.大数据的文件读取 ① 利用生成器generator ②迭代器进行迭代遍历:for line in ...