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


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

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

    1.      private void indexLocation(Document document, JSONObject jo)  
    2.         throws Exception {  
    3.   
    4.     double longitude = jo.getDouble("longitude");  
    5.     double latitude = jo.getDouble("latitude");  
    6.   
    7.     document.add(new Field("lat", NumericUtils  
    8.             .doubleToPrefixCoded(latitude), Field.Store.YES,  
    9.             Field.Index.NOT_ANALYZED));  
    10.     document.add(new Field("lng", NumericUtils  
    11.             .doubleToPrefixCoded(longitude), Field.Store.YES,  
    12.             Field.Index.NOT_ANALYZED));  
    13.   
    14.     for (int tier = startTier; tier <= endTier; tier++) {  
    15.         ctp = new CartesianTierPlotter(tier, projector,  
    16.                 CartesianTierPlotter.DEFALT_FIELD_PREFIX);  
    17.         final double boxId = ctp.getTierBoxId(latitude, longitude);  
    18.         document.add(new Field(ctp.getTierFieldName(), NumericUtils  
    19.                 .doubleToPrefixCoded(boxId), Field.Store.YES,  
    20.                 Field.Index.NOT_ANALYZED_NO_NORMS));  
    21.     }  
    22. }  
          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.   
    4. import java.util.List;  
    5.   
    6. import org.junit.Test;  
    7.   
    8. public class LuceneSpatialTest {  
    9.       
    10.     private static LuceneSpatial spatialSearcher = new LuceneSpatial();  
    11.   
    12.     @Test  
    13.     public void testSearch() {  
    14.         try {  
    15.             long start = System.currentTimeMillis();  
    16.             List<String> results = spatialSearcher.search("美发"116.383818339.96290153.0);  
    17.             System.out.println(results.size()  
    18.                     + "个匹配结果,共耗时 "  
    19.                     + (System.currentTimeMillis() - start) + "毫秒。 ");  
    20.             assertEquals(4, results.size());  
    21.         } catch (Exception e) {  
    22.             fail("Exception occurs...");  
    23.             e.printStackTrace();  
    24.         }  
    25.     }  
    26.   
    27.     @Test  
    28.     public void testSearchWithoutKeyword() {  
    29.         try {  
    30.             long start = System.currentTimeMillis();  
    31.             List<String> results = spatialSearcher.search(null116.383818339.96290153.0);  
    32.             System.out.println( results.size()  
    33.                     + "个匹配结果,共耗时 "  
    34.                     + (System.currentTimeMillis() - start) + "毫秒. ");  
    35.             assertEquals(6, results.size());  
    36.         } catch (Exception e) {  
    37.             fail("Exception occurs...");  
    38.             e.printStackTrace();  
    39.         }  
    40.     }  
    41. }  
    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) + "毫秒。
    ");
    			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) + "毫秒.
    ");
    			assertEquals(6, results.size());
    		} catch (Exception e) {
    			fail("Exception occurs...");
    			e.printStackTrace();
    		}
    	}
    }


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

    1. public class LuceneSpatial {  
    2.   
    3.     private Analyzer analyzer;  
    4.     private IndexWriter writer;  
    5.     private FSDirectory indexDirectory;  
    6.     private IndexSearcher indexSearcher;  
    7.     private IndexReader indexReader;  
    8.     private String indexPath = "c:/lucene-spatial";  
    9.   
    10.     // Spatial  
    11.     private IProjector projector;  
    12.     private CartesianTierPlotter ctp;  
    13.     public static final double RATE_MILE_TO_KM = 1.609344//英里和公里的比率  
    14.     public static final String LAT_FIELD = "lat";  
    15.     public static final String LON_FIELD = "lng";  
    16.     private static final double MAX_RANGE = 15.0// 索引支持的最大范围,单位是千米  
    17.     private static final double MIN_RANGE = 3.0;  // 索引支持的最小范围,单位是千米  
    18.     private int startTier;  
    19.     private int endTier;  
    20.   
    21.     public LuceneSpatial() {  
    22.         try {  
    23.             init();  
    24.         } catch (Exception e) {  
    25.             e.printStackTrace();  
    26.         }  
    27.     }  
    28.   
    29.     private void init() throws Exception {  
    30.         initializeSpatialOptions();  
    31.   
    32.         analyzer = new StandardAnalyzer(Version.LUCENE_32);  
    33.   
    34.         File path = new File(indexPath);  
    35.   
    36.         boolean isNeedCreateIndex = false;  
    37.   
    38.         if (path.exists() && !path.isDirectory())  
    39.             throw new Exception("Specified path is not a directory");  
    40.   
    41.         if (!path.exists()) {  
    42.             path.mkdirs();  
    43.             isNeedCreateIndex = true;  
    44.         }  
    45.   
    46.         indexDirectory = FSDirectory.open(new File(indexPath));  
    47.   
    48.         //建立索引  
    49.         if (isNeedCreateIndex) {  
    50.             IndexWriterConfig indexWriterConfig = new IndexWriterConfig(  
    51.                     Version.LUCENE_32, analyzer);  
    52.             indexWriterConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);  
    53.             writer = new IndexWriter(indexDirectory, indexWriterConfig);  
    54.             buildIndex();  
    55.         }  
    56.   
    57.         indexReader = IndexReader.open(indexDirectory, true);  
    58.         indexSearcher = new IndexSearcher(indexReader);  
    59.   
    60.     }  
    61.   
    62.     @SuppressWarnings("deprecation")  
    63.     private void initializeSpatialOptions() {  
    64.         projector = new SinusoidalProjector();  
    65.         ctp = new CartesianTierPlotter(0, projector,  
    66.                 CartesianTierPlotter.DEFALT_FIELD_PREFIX);  
    67.         startTier = ctp.bestFit(MAX_RANGE / RATE_MILE_TO_KM);  
    68.         endTier = ctp.bestFit(MIN_RANGE / RATE_MILE_TO_KM);  
    69.     }  
    70.   
    71.   
    72.   
    73.     private int mile2Meter(double miles) {  
    74.         double dMeter = miles * RATE_MILE_TO_KM * 1000;  
    75.   
    76.         return (int) dMeter;  
    77.     }  
    78.   
    79.     private double km2Mile(double km) {  
    80.         return km / RATE_MILE_TO_KM;  
    81.     }  
    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.   
    12.         writer.commit();  
    13.     } catch (Exception e) {  
    14.         e.printStackTrace();  
    15.     } finally {  
    16.         if (br != null) {  
    17.             try {  
    18.                 br.close();  
    19.             } catch (IOException e) {  
    20.                 e.printStackTrace();  
    21.             }  
    22.         }  
    23.     }  
    24. }  
    25.   
    26. private void index(JSONObject jo) throws Exception {  
    27.     Document doc = new Document();  
    28.   
    29.     doc.add(new Field("id", jo.getString("id"), Field.Store.YES,  
    30.             Field.Index.ANALYZED));  
    31.   
    32.     doc.add(new Field("title", jo.getString("title"), Field.Store.YES,  
    33.             Field.Index.ANALYZED));  
    34.   
    35.     //将位置信息添加到索引中  
    36.     indexLocation(doc, jo);  
    37.   
    38.     writer.addDocument(doc);  
    39. }  
    40.   
    41. private void indexLocation(Document document, JSONObject jo)  
    42.         throws Exception {  
    43.   
    44.     double longitude = jo.getDouble("longitude");  
    45.     double latitude = jo.getDouble("latitude");  
    46.   
    47.     document.add(new Field("lat", NumericUtils  
    48.             .doubleToPrefixCoded(latitude), Field.Store.YES,  
    49.             Field.Index.NOT_ANALYZED));  
    50.     document.add(new Field("lng", NumericUtils  
    51.             .doubleToPrefixCoded(longitude), Field.Store.YES,  
    52.             Field.Index.NOT_ANALYZED));  
    53.   
    54.     for (int tier = startTier; tier <= endTier; tier++) {  
    55.         ctp = new CartesianTierPlotter(tier, projector,  
    56.                 CartesianTierPlotter.DEFALT_FIELD_PREFIX);  
    57.         final double boxId = ctp.getTierBoxId(latitude, longitude);  
    58.         document.add(new Field(ctp.getTierFieldName(), NumericUtils  
    59.                 .doubleToPrefixCoded(boxId), Field.Store.YES,  
    60.                 Field.Index.NOT_ANALYZED_NO_NORMS));  
    61.     }  
    62. }  
    	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.   
    5.     double miles = km2Mile(range);  
    6.       
    7.     DistanceQueryBuilder dq = new DistanceQueryBuilder(latitude,  
    8.             longitude, miles, "lat""lng",  
    9.             CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, startTier,  
    10.             endTier);  
    11.   
    12.     //按照距离排序  
    13.     DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(  
    14.             dq.getDistanceFilter());  
    15.     Sort sort = new Sort(new SortField("geo_distance", dsort));  
    16.   
    17.     Query query = buildQuery(keyword);  
    18.   
    19.     //搜索结果  
    20.     TopDocs hits = indexSearcher.search(query, dq.getFilter(),  
    21.             Integer.MAX_VALUE, sort);  
    22.     //获得各条结果相对应的距离  
    23.     Map<Integer, Double> distances = dq.getDistanceFilter()  
    24.             .getDistances();  
    25.   
    26.     for (int i = 0; i < hits.totalHits; i++) {  
    27.         final int docID = hits.scoreDocs[i].doc;  
    28.   
    29.         final Document doc = indexSearcher.doc(docID);  
    30.   
    31.         final StringBuilder builder = new StringBuilder();  
    32.         builder.append("找到了: ")  
    33.                 .append(doc.get("title"))  
    34.                 .append(", 距离: ")  
    35.                 .append(mile2Meter(distances.get(docID)))  
    36.                 .append("米。");  
    37.         System.out.println(builder.toString());  
    38.   
    39.         result.add(builder.toString());  
    40.     }  
    41.   
    42.     return result;  
    43. }  
    44.   
    45. private Query buildQuery(String keyword) throws Exception {  
    46.     //如果没有指定关键字,则返回范围内的所有结果  
    47.     if (keyword == null || keyword.isEmpty()) {  
    48.         return new MatchAllDocsQuery();  
    49.     }  
    50.     QueryParser parser = new QueryParser(Version.LUCENE_32, "title",  
    51.             analyzer);  
    52.   
    53.     parser.setDefaultOperator(Operator.AND);  
    54.   
    55.     return parser.parse(keyword.toString());  
    56. }  
    	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.   
    7. 找到了: 时尚码头美容美发热烫特价, 距离: 0米。  
    8. 找到了: 仅98元!享原价335元李老爹, 距离: 68米。  
    9. 找到了: 海底捞吃300送300, 距离: 210米。  
    10. 找到了: 都美造型烫染美发护理套餐, 距离: 210米。  
    11. 找到了: 审美个人美容美发套餐, 距离: 426米。  
    12. 找到了: 小区东门时尚烫染个人护理美发套餐, 距离: 439米。  
    13. 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

  • 相关阅读:
    Activiti6-IdentityService(学习笔记)
    Activiti6-TaskService(学习笔记)重要
    选定用户与用户组启动流程(学习笔记)
    Activiti6作业执行器Job Executor配置(学习笔记)
    命令模式与责任链模式以及命令拦截器的配置(学习笔记)
    Activiti6事件及监听器配置(学习笔记)
    基于MySQL的Activiti6引擎创建
    abowman
    wpf 遍历listview 时 传入指定类型 得到指定类型控件info
    wpf listview 行变色
  • 原文地址:https://www.cnblogs.com/dead-trap-ramble/p/3637900.html
Copyright © 2020-2023  润新知